1. はじめに
前回に引き続き、データサイエンス100本ノックの解説を行う。
[データサイエンス100本ノック解説(P001~020)] (https://qiita.com/ProgramWataru/items/42ff579a3cdb4f0ad158)
データサイエンス100本ノック解説(P021~040)
データサイエンス100本ノック解説(P041~060)
データサイエンス100本ノック解説(P061~080)
導入についてはこちらの記事を参考に進めてください(※ MacでDockerを扱います)
2. 解説編
P-081: 単価(unit_price)と原価(unit_cost)の欠損値について、それぞれの平均値で補完した新たなdf_product_2を作成せよ。なお、平均値について1円未満は四捨五入とし、0.5については偶数寄せでかまわない。補完実施後、各項目について欠損が生じていないことも確認すること。
# fillnaで欠損値を補完する。
# np.roundで偶数よせの四捨五入。np.nanmeanで欠損値の値を除いた平均値
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']))})
# isnull().sum()で欠損を確認する
df_product2.isnull().sum()
参考: pandasで数値を丸める(四捨五入、偶数への丸め)
参考: NumPyで欠損値np.nanを含む配列ndarrayの合計や平均を算出
P-082: 単価(unit_price)と原価(unit_cost)の欠損値について、それぞれの中央値で補完した新たなdf_product_3を作成せよ。なお、中央値について1円未満は四捨五入とし、0.5については偶数寄せでかまわない。補完実施後、各項目について欠損が生じていないことも確認すること。
# 欠損値はfillnaを用いる
# np.roundで偶数よせの四捨五入。中央値np.nanmedianで補完する
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']))})
# isnull().sum()で欠損を確認する
df_product_3.isnull().sum()
参考: pandasで数値を丸める(四捨五入、偶数への丸め)
参考: [NumPy] 11. NumPy配列のNaNに関するいろいろな処理
P-083: 単価(unit_price)と原価(unit_cost)の欠損値について、各商品の小区分(category_small_cd)ごとに算出した中央値で補完した新たなdf_product_4を作成せよ。なお、中央値について1円未満は四捨五入とし、0.5については偶数寄せでかまわない。補完実施後、各項目について欠損が生じていないことも確認すること。
# (解法1)
# 各商品の小区分(category_small_cd)ごとに中央値を出す
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)と中央値が入ったデータフレーム(df_tmp)を結合する
df_product_4 = pd.merge(df_product, df_tmp, how='inner', on='category_small_cd')
# 単価(unit_price)の欠損値に中央値を補完する(axis=1で各行に適用)
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)
# 原価(unit_cost)の欠損値に中央値を補完する(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)
# isnull().sum()で欠損を確認する
df_product_4.isnull().sum()
# (解法2)
# 各商品の小区分(category_small_cd)ごとに中央値を出す
df_tmp = df_product.groupby('category_small_cd').agg(median_price=('unit_price', 'median'),
median_cost=('unit_cost', 'median')).reset_index()
# 商品データフレーム(df_product)と中央値が入ったデータフレーム(df_tmp)を結合する
df_product_4 = pd.merge(df_product, df_tmp, how='inner', on='category_small_cd')
# maskを用いて欠損値(Nan)の部分はマスク(見えないように)して、中央値を代入する
# mask(隠す条件, 埋める値)、roundで四捨五入
df_product_4['unit_price'] = (df_product_4['unit_price'].mask(df_product_4['unit_price'].isnull(),
df_product_4['median_price'].round()))
df_product_4['unit_cost'] = (df_product_4['unit_cost'].mask(df_product_4['unit_cost'].isnull(),
df_product_4['median_price'].round()))
# isnull().sum()で欠損を確認する
df_product_4.isnull().sum()
# (解法3)
# 商品データフレーム(df_product)をコピーする
df_product_4 = df_product.copy()
# 単価(unit_price)と原価(unit_cost)それぞれの処理を行う
# fillnaを用いて欠損値を補完する。
# 各商品の小区分(category_small_cd)ごとに中央値を出す。transformを用いる。
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())
# isnull().sum()で欠損を確認する
df_product_4.isnull().sum()
参考: [NumPy] 11. NumPy配列のNaNに関するいろいろな処理
参考: Python pandas データ選択処理をちょっと詳しく <後編>
参考: Pandas の transform と apply の基本的な違い
P-084: 顧客データフレーム(df_customer)の全顧客に対し、全期間の売上金額に占める2019年売上金額の割合を計算せよ。ただし、販売実績のない場合は0として扱うこと。そして計算した割合が0超のものを抽出せよ。 結果は10件表示させれば良い。また、作成したデータにNAやNANが存在しないことを確認せよ。
# レシート明細データフレーム(df_receipt)のsales_ymdが2019年のものを抽出する(df_tmp1)
df_tmp_1 = df_receipt.query('20190101 <= sales_ymd <= 20191231')
# 顧客データフレーム(df_customer)のcustomer_idと2019年のみを抽出したレシート明細データフレーム(df_tmp1)を左結合
# customer_idごとにグループ分け(結合時に増えている)し、売上金額(amount)を合計する
# カラム名を変更する(rename)
df_tmp_1 = pd.merge(df_customer['customer_id'], df_tmp_1[['customer_id', 'amount']], how='left', on='customer_id'). \
groupby('customer_id').sum().reset_index().rename(columns={'amount': 'amount_2019'})
# 全期間の売上金額を顧客別に合計する
df_tmp_2 = pd.merge(df_customer['customer_id'], df_receipt[['customer_id', 'amount']], how='left', on='customer_id'). \
groupby('customer_id').sum().reset_index()
# df_tmp1とdf_tmp2を結合する(2019年と全ての期間の売上高(amount)の比較用)
df_tmp = pd.merge(df_tmp1, df_tmp_2, how='inner', on='customer_id')
# # 欠損値をそれぞれ0で補完する
df_tmp['amount_2019'] = df_tmp['amount_2019'].fillna(0)
df_tmp['amount'] = df_tmp['amount'].fillna(0)
# 割合(amount_rate)を求め、欠損値を補完する
df_tmp['amount_rate'] = df_tmp['amount_2019'] / df_tmp['amount']
df_tmp['amount_rate'] = df_tmp['amount_rate'].fillna(0)
# 割合(amount_rate)が0を超えるものを10件表示する
df_tmp.query('amount_rate > 0').head(10)
参考: pandasの使い方(merge、join、concat編)
参考: pandas.DataFrameの行名・列名の変更
P-085: 顧客データフレーム(df_customer)の全顧客に対し、郵便番号(postal_cd)を用いて経度緯度変換用データフレーム(df_geocode)を紐付け、新たなdf_customer_1を作成せよ。ただし、複数紐づく場合は経度(longitude)、緯度(latitude)それぞれ平均を算出すること。
# 顧客データフレーム(df_customer)と経度緯度変換用データフレーム(df_geocode)を結合する
df_customer_1 = pd.merge(df_customer[['customer_id', 'postal_cd']],
df_geocode[['postal_cd', 'longitude' ,'latitude']],
how='inner', on='postal_cd')
# 経度緯度変換用データフレームに重複があるため、複数紐づいている。
# customer_idでグループ分けし、経度(longitude)、緯度(latitude)それぞれ平均
# カラム名を変更する
df_customer_1 = df_customer_1.groupby('customer_id'). \
agg({'longitude':'mean', 'latitude':'mean'}).reset_index(). \
rename(columns={'longitude':'m_longitude', 'latitude':'m_latitude'})
# 顧客データフレーム(df_customer)と重複がない(df_customer_1)を結合する
df_customer_1 = pd.merge(df_customer, df_customer_1, how='inner', on='customer_id')
df_customer_1.head(3)
参考: pandasの使い方(merge、join、concat編)
参考: pandas.DataFrameの行名・列名の変更
P-086: 前設問で作成した緯度経度つき顧客データフレーム(df_customer_1)に対し、申込み店舗コード(application_store_cd)をキーに店舗データフレーム(df_store)と結合せよ。そして申込み店舗の緯度(latitude)・経度情報(longitude)と顧客の緯度・経度を用いて距離(km)を求め、顧客ID(customer_id)、顧客住所(address)、店舗住所(address)とともに表示せよ。計算式は簡易式で良いものとするが、その他精度の高い方式を利用したライブラリを利用してもかまわない。結果は10件表示すれば良い。
$$
緯度(ラジアン):\phi \
経度(ラジアン):\lambda \
$$
$$
距離L = 6371 * arccos(sin \phi_1 * sin \phi_2
- cos \phi_1 * cos \phi_2 * cos(\lambda_1 − \lambda_2))
$$
# 計算式の関数を作成
def calc_distance(x1, y1, x2, y2):
x1_r = np.radians(x1)
x2_r = np.radians(x2)
y1_r = np.radians(y1)
y2_r = np.radians(y2)
return 6371 * np.arccos(np.sin(y1_r) * np.sin(y2_r)
+ np.cos(y1_r) * np.cos(y2_r)
* np.cos(x1_r - x2_r))
# 緯度経度つき顧客データフレーム(df_customer_1)と店舗データフレーム(df_store)と結合
df_tmp = pd.merge(df_customer_1, df_store, how='inner',
left_on=['application_store_cd'], right_on=['store_cd'])
# 距離を求める
df_tmp['distance'] = calc_distance(df_tmp['m_longitude'], df_tmp['m_latitude'],
df_tmp['longitude'], df_tmp['latitude'])
df_tmp[['customer_id', 'address_x', 'address_y', 'distance']].head(10)
# (別解) mathを使ったパターン
def calc_distance(x1, y1, x2, y2):
distance = 6371 * math.acos(math.sin(math.radians(y1)) * math.sin(math.radians(y2))
+ math.cos(math.radians(y1)) * math.cos(math.radians(y2))
* math.cos(math.radians(x1) - math.radians(x2)))
return distance
df_tmp = pd.merge(df_customer_1, df_store, how='inner', left_on='application_store_cd', right_on='store_cd')
df_tmp['distance'] = df_tmp[['m_longitude', 'm_latitude','longitude', 'latitude']]. \
apply(lambda x: calc_distance(x[0], x[1], x[2], x[3]), axis=1)
df_tmp[['customer_id', 'address_x', 'address_y', 'distance']].head(10)
P-087: 顧客データフレーム(df_customer)では、異なる店舗での申込みなどにより同一顧客が複数登録されている。名前(customer_name)と郵便番号(postal_cd)が同じ顧客は同一顧客とみなし、1顧客1レコードとなるように名寄せした名寄顧客データフレーム(df_customer_u)を作成せよ。ただし、同一顧客に対しては売上金額合計が最も高いものを残すものとし、売上金額合計が同一もしくは売上実績の無い顧客については顧客ID(customer_id)の番号が小さいものを残すこととする。
# レシート明細データフレーム(df_receipt)を顧客ごとにグループ分けして売り上げ金額を合計する
df_tmp = df_receipt.groupby('customer_id').agg({'amount': sum}).reset_index()
# 顧客データフレーム(df_customer)と先ほど作成した顧客ごとの売り上げ金額を結合する
# 顧客データフレーム(df_customer)のキーを全て残すため左結合(how='left')
# 顧客ごとに昇順を行い、金額を降順でソートする
df_customer_u = pd.merge(df_customer, df_tmp, how='left', on='customer_id'). \
sort_values(['amount', 'customer_id'], ascending=[False, True])
# 1顧客1レコードとなるように名前(customer_name)と郵便番号(postal_cd)の重複を削除する。(ソートした結果、重複のものは最初のものを残す)
df_customer_u.drop_duplicates(subset=['customer_name', 'postal_cd'], keep='first', inplace=True)
# 減少した数を出力する
print('減少数: ', len(df_customer) - len(df_customer_u))
参考: Python pandas 図でみる データ連結 / 結合処理
参考: pandas.DataFrame, Seriesをソートするsort_values, sort_index
参考: pandas.DataFrame, Seriesの重複した行を抽出・削除
P-088: 前設問で作成したデータを元に、顧客データフレームに統合名寄IDを付与したデータフレーム(df_customer_n)を作成せよ。ただし、統合名寄IDは以下の仕様で付与するものとする。
重複していない顧客:顧客ID(customer_id)を設定
重複している顧客:前設問で抽出したレコードの顧客IDを設定
# 顧客データフレーム(df_customer)と名寄せした名寄顧客データフレーム(df_customer_u)を結合する
# ※名前(customer_name)と郵便番号(postal_cd)が一致するものを結合する
df_customer_n = pd.merge(df_customer, df_customer_u[['customer_id', 'customer_name', 'postal_cd']],
how='inner', on=['customer_name', 'postal_cd'])
# カラム名を変更
df_customer_n.rename(columns={'customer_id_x': 'customer_id', 'customer_id_y': 'integration_id'}, inplace=True)
# IDの差を求める
print('ID数の差', len(df_customer_n['customer_id'].unique()) - len(df_customer_n['integration_id'].unique()))
参考: pandasでユニークな要素の個数、頻度(出現回数)をカウント
P-089: 売上実績のある顧客に対し、予測モデル構築のため学習用データとテスト用データに分割したい。それぞれ8:2の割合でランダムにデータを分割せよ。
# 売り上げ実績のある顧客データを作成(df_customerとdf_receiptを結合)
df_tmp = pd.merge(df_customer, df_receipt['customer_id'], how='inner', on='customer_id')
# df_customerを学習用データとテスト用データに分割 (train_test_split)を用いる
df_train, df_test = train_test_split(df_tmp, test_size=0.2, random_state=71)
# 学習用データとテスト用データそれぞれの割合を出す
print('学習データ割合: ', len(df_train) / len(df_tmp))
print('テストデータ割合: ', len(df_test) / len(df_tmp))
参考: scikit-learnでデータを訓練用とテスト用に分割するtrain_test_split
P-090: レシート明細データフレーム(df_receipt)は2017年1月1日〜2019年10月31日までのデータを有している。売上金額(amount)を月次で集計し、学習用に12ヶ月、テスト用に6ヶ月のモデル構築用データを3セット作成せよ。
# レシート明細データフレーム(df_receipt)をコピーする
df_tmp = df_receipt.copy()
# sales_ymdをint64からstr型へ変更し、スライスを用いて年月を取り出す
df_tmp['sales_ymd'] = df_tmp['sales_ymd'].astype('str').str[:6]
# sales_ymd(年月)でグループ分けして、売上金額(amount)を合計する
df_tmp = df_tmp.groupby('sales_ymd').agg({'amount': sum}).reset_index()
# 学習データとテストデータに分ける関数
def split_date(df, train_size, test_size, slide_window, start_point):
train_start = start_point * slide_window
test_start = train_start + train_size
return df[train_start:test_start], df[test_start:test_start+test_size]
# モデル構築用データを3セット作成
df_train_1, df_test_1 = split_date(df_tmp, train_size=12, test_size=6, slide_window=6, start_point=0)
df_train_2, df_test_2 = split_date(df_tmp, train_size=12, test_size=6, slide_window=6, start_point=1)
df_train_3, df_test_3 = split_date(df_tmp, train_size=12, test_size=6, slide_window=6, start_point=2)
(補足: 問題を理解する)
学習用は12ヶ月データでテスト用は6ヶ月データとするモデルは3つ
モデル | 学習データ | 学習データ範囲 | テストデータ | テストデータ範囲 |
---|---|---|---|---|
モデル1 | df_train_1 | 2017/01月〜2017/12月 | df_train_1 | 2017/01月〜2017/12月 |
モデル2 | df_train_2 | 2017/06月〜2018/06月 | df_train_2 | 2018/06月〜2018/12月 |
モデル3 | df_train_3 | 2018/01月〜2018/12月 | df_train_3 | 2019/01月〜2019/06月 |
参考: scikit-learnでデータを訓練用とテスト用に分割するtrain_test_split
P-091: 顧客データフレーム(df_customer)の各顧客に対し、売上実績のある顧客数と売上実績のない顧客数が1:1となるようにアンダーサンプリングで抽出せよ。
# レシート明細データフレーム(df_receipt)を顧客ごとにグループ分けし、売り上げを合計する
df_tmp = df_receipt.groupby('customer_id').agg({'amount': sum}).reset_index()
# 顧客データフレーム(df_customer)とレシート明細データフレーム(df_receipt)を結合(左結合)
df_tmp = pd.merge(df_customer, df_tmp, how='left', on='customer_id')
# 売り上げ実績(amount)がある場合は1、ない場合は0を入れる
df_tmp['buy_flg'] = df_tmp['amount'].apply(lambda x: 0 if np.isnan(x) else 1)
# 売上実績のある顧客数と売上実績のない顧客数の件数を出力する
print('0の件数', len(df_tmp.query('buy_flg==0')))
print('1の件数', len(df_tmp.query('buy_flg==1')))
# RandomUnderSampler でアンダーサンプリングする
rs = RandomUnderSampler(random_state=71)
df_sample, _ = rs.fit_sample(df_tmp, df_tmp.buy_flg)
# 売上実績のある顧客数と売上実績のない顧客数の件数を出力する
print('0の件数', len(df_sample.query('buy_flg==0')))
print('1の件数', len(df_sample.query('buy_flg==1')))
参考: 【Kaggle】imbalanced-learn を使ってアンダーサンプリングをしてみた
参考: 多クラス分類の不均衡データのdownsampling
P-092: 顧客データフレーム(df_customer)では、性別に関する情報が非正規化の状態で保持されている。これを第三正規化せよ。
# 顧客データフレーム(df_customer)のgender_cdの重複を取り除く
df_gender = df_customer[['gender_cd', 'gender']].drop_duplicates()
# 性別のデータフレーム(df_gender)を作成したので顧客データフレームからgenderカラムを削除する
df_customer_s = df_customer.drop(columns='gender')
参考: pandas.DataFrame, Seriesの重複した行を抽出・削除
P-093: 商品データフレーム(df_product)では各カテゴリのコード値だけを保有し、カテゴリ名は保有していない。カテゴリデータフレーム(df_category)と組み合わせて非正規化し、カテゴリ名を保有した新たな商品データフレームを作成せよ。
# 商品データフレーム(df_product)とカテゴリデータフレーム(df_category)を結合する
df_product_full = pd.merge(df_product,df_category[['category_small_cd', 'category_major_name',
'category_medium_name', 'category_small_name']],
how='inner', on='category_small_cd')
df_product_full.head()
キーを"category_small_cd"としているのは、"category_major_cd"や"category_medium_cd"ではカテゴリーとなっており、細かく分類できないからである。
P-094: 先に作成したカテゴリ名付き商品データを以下の仕様でファイル出力せよ。なお、出力先のパスはdata配下とする。
・ファイル形式はCSV(カンマ区切り)
・ヘッダ有り
・文字コードはUTF-8
# to_csv(ファイル形式はCSV)を用いて出力する
df_product_full.to_csv('./data/P_df_product_full_UTF-8_header.csv', encoding='UTF-8', index=False)
# (別解)
# コード例2(BOM付きでExcelの文字化けを防ぐ)
df_product_full.to_csv('./data/P_df_product_full_UTF-8_header.csv',
encoding='utf_8_sig',
index=False)
参考: pandasでcsvファイルの書き出し・追記(to_csv)
P-095: 先に作成したカテゴリ名付き商品データを以下の仕様でファイル出力せよ。なお、出力先のパスはdata配下とする。
・ファイル形式はCSV(カンマ区切り)
・ヘッダ有り
・文字コードはCP932
# to_csv(ファイル形式はCSV)を用いて出力する
df_product_full.to_csv('./data/P_df_product_full_CP932_header.csv', encoding='CP932', index=False)
CP932とは?
Microsoft コードページ 932(CP932)は、マイクロソフト及び、MS-DOSのOEMベンダがShift_JISを独自に拡張した文字コードである。
参考: pandasでcsvファイルの書き出し・追記(to_csv)
P-096: 先に作成したカテゴリ名付き商品データを以下の仕様でファイル出力せよ。なお、出力先のパスはdata配下とする。
・ファイル形式はCSV(カンマ区切り)
・ヘッダ無し
・文字コードはUTF-8
# to_csv(ファイル形式はCSV)を用いて出力する
df_product_full.to_csv('./data/P_df_product_full_UTF-8_noh.csv', header=False ,encoding='UTF-8', index=False)
参考: pandasでcsvファイルの書き出し・追記(to_csv)
P-097: 先に作成した以下形式のファイルを読み込み、データフレームを作成せよ。また、先頭10件を表示させ、正しくとりまれていることを確認せよ。
・ファイル形式はCSV(カンマ区切り)
・ヘッダ有り
・文字コードはUTF-8
# read_csvを用いて読み込む
df_tmp = pd.read_csv('./data/P_df_product_full_UTF-8_header.csv')
df_tmp.head(10)
参考: pandasでcsv/tsvファイル読み込み(read_csv, read_table)
P-098: 先に作成した以下形式のファイルを読み込み、データフレームを作成せよ。また、先頭10件を表示させ、正しくとりまれていることを確認せよ。
・ファイル形式はCSV(カンマ区切り)
・ヘッダ無し
・文字コードはUTF-8
# read_csvを用いて読み込む
df_tmp = pd.read_csv('./data/P_df_product_full_UTF-8_noh.csv', header=None)
df_tmp.head(10)
参考: pandasでcsv/tsvファイル読み込み(read_csv, read_table)
P-099: 先に作成したカテゴリ名付き商品データを以下の仕様でファイル出力せよ。なお、出力先のパスはdata配下とする。
・ファイル形式はTSV(タブ区切り)
・ヘッダ有り
・文字コードはUTF-8
# to_csv(ファイル形式はCSV)を用いて出力する
# タブ文字\tで区切ったtsvファイルとして保存
df_product_full.to_csv('./data/P_df_product_full_UTF-8_header.tsv', sep='\t', encoding='UTF-8', index=False)
参考: pandasでcsvファイルの書き出し・追記(to_csv)
P-100: 先に作成した以下形式のファイルを読み込み、データフレームを作成せよ。また、先頭10件を表示させ、正しくとりまれていることを確認せよ。
・ファイル形式はTSV(タブ区切り)
・ヘッダ有り
・文字コードはUTF-8
# TSVの読み込みはread_tableを用いる
df_tmp = pd.read_table('./data/P_df_product_full_UTF-8_header.tsv', encoding='utf-8')
df_tmp.head(10)
参考: pandasでcsv/tsvファイル読み込み(read_csv, read_table)
3. 参考文献
データサイエンス100本ノック
Macでデータサイエンス100本ノックを動かす方法
4. 所感
081-093までは非常に歯応えがある内容であった。
解説が浅いところもあるので復習しながら解説を充実させていきます。