だから僕はpandasを辞めた【データサイエンス100本ノック(構造化データ加工編)篇 #5】
データサイエンス100本ノック(構造化データ加工編)のPythonの問題を解いていきます。この問題群は、模範解答ではpandasを使ってデータ加工を行っていますが、私達は勉強がてらにNumPyを用いて処理していきます。
はじめに
NumPyの勉強として、データサイエンス100本ノック(構造化データ加工編)のPythonの問題を解いていきます。
Pythonでデータサイエンス的なことをする人の多くはpandas大好き人間かもしれませんが、実はpandasを使わなくても、NumPyで同じことができます。そしてNumPyの方がたいてい高速です。
pandas大好き人間だった僕もNumPyの操作には依然として慣れていないので、今回この『データサイエンス100本ノック』をNumPyで操作することでpandasからの卒業を試みて行きたいと思います。
今回は36~44問目をやっていきます。複数のデータフレームのマージというテーマのようです。
初期データは以下のようにして読み込みました。
import numpy as np
import pandas as pd
from numpy.lib import recfunctions as rfn
# 模範解答用
df_category = pd.read_csv('data/category.csv', dtype='string')
df_customer = pd.read_csv('data/customer.csv')
df_product = pd.read_csv(
'data/product.csv',
dtype={col: 'string' for col in
['category_major_cd', 'category_medium_cd', 'category_small_cd']})
df_receipt = pd.read_csv('data/receipt.csv')
df_store = pd.read_csv('data/store.csv')
# 僕たちが扱うデータ
arr_category = np.genfromtxt(
'data/category.csv', delimiter=',', encoding='utf-8-sig',
names=True, dtype=tuple(['<U15']*6))
arr_customer = np.genfromtxt(
'data/customer.csv', delimiter=',', encoding='utf-8',
names=True, dtype=None)
arr_product = np.genfromtxt(
'data/product.csv', delimiter=',', encoding='utf-8-sig',
names=True, dtype=tuple(['<U10']*4+['<i4']*2))
arr_receipt = np.genfromtxt(
'data/receipt.csv', delimiter=',', encoding='utf-8',
names=True, dtype=None)
arr_store = np.genfromtxt(
'data/store.csv', delimiter=',', encoding='utf-8',
names=True, dtype=None)
def make_array(size, **kwargs):
arr = np.empty(size, dtype=[(colname, subarr.dtype)
for colname, subarr in kwargs.items()])
for colname, subarr in kwargs.items():
arr[colname] = subarr
return arr
P_036
P-036: レシート明細データフレーム(df_receipt)と店舗データフレーム(df_store)を内部結合し、レシート明細データフレームの全項目と店舗データフレームの店舗名(store_name)を10件表示させよ。
内部結合ですが、内容はVLOOKUPです。まず両フレームのキー列を合成した行列に対してnp.unique()
を行って数値データに変換します。つづいてnp.searchsorted()
を使ってレシート明細の店舗コード列を置換していきます。
_, inv = np.unique(np.concatenate([arr_store['store_cd'],
arr_receipt['store_cd']]),
return_inverse=True)
inv_map, inv_arr = inv[:arr_store.size], inv[arr_store.size:]
sorter_index = np.argsort(inv_map)
idx = np.searchsorted(inv_map, inv_arr, sorter=sorter_index)
store_name = arr_store['store_name'][sorter_index[idx]]
new_arr = make_array(arr_receipt.size, **{col: arr_receipt[col]
for col in arr_receipt.dtype.names},
store_name=store_name)
new_arr[:10]
array([(20181103, 1257206400, 'S14006', 112, 1, 'CS006214000001', 'P070305012', 1, 158, '葛が谷店'),
(20181118, 1258502400, 'S13008', 1132, 2, 'CS008415000097', 'P070701017', 1, 81, '成城店'),
(20170712, 1215820800, 'S14028', 1102, 1, 'CS028414000014', 'P060101005', 1, 170, '二ツ橋店'),
(20190205, 1265328000, 'S14042', 1132, 1, 'ZZ000000000000', 'P050301001', 1, 25, '新山下店'),
(20180821, 1250812800, 'S14025', 1102, 2, 'CS025415000050', 'P060102007', 1, 90, '大和店'),
(20190605, 1275696000, 'S13003', 1112, 1, 'CS003515000195', 'P050102002', 1, 138, '狛江店'),
(20181205, 1259971200, 'S14024', 1102, 2, 'CS024514000042', 'P080101005', 1, 30, '三田店'),
(20190922, 1285113600, 'S14040', 1102, 1, 'CS040415000178', 'P070501004', 1, 128, '長津田店'),
(20170504, 1209859200, 'S13020', 1112, 2, 'ZZ000000000000', 'P071302010', 1, 770, '十条仲原店'),
(20191010, 1286668800, 'S14027', 1102, 1, 'CS027514000015', 'P071101003', 1, 680, '南藤沢店')],
dtype=[('sales_ymd', '<i4'), ('sales_epoch', '<i4'), ('store_cd', '<U6'), ('receipt_no', '<i4'), ('receipt_sub_no', '<i4'), ('customer_id', '<U14'), ('product_cd', '<U10'), ('quantity', '<i4'), ('amount', '<i4'), ('store_name', '<U6')])
模範解答のpd.merge()
を用いた方法の場合、強制的にキー列でソートされてしまう(キー列がユニークでないため)。
今回の問題であれば、pandasで行う場合は
store_name = df_receipt['store_cd'].map(
df_store.set_index('store_cd')['store_name'])
df = df_receipt.assign(store_name=store_name)
とすれば良い。
P_037
P-037: 商品データフレーム(df_product)とカテゴリデータフレーム(df_category)を内部結合し、商品データフレームの全項目とカテゴリデータフレームの小区分名(category_small_name)を10件表示させよ。
同じ。
_, inv = np.unique(np.concatenate([arr_category['category_small_cd'],
arr_product['category_small_cd']]),
return_inverse=True)
inv_map, inv_arr = inv[:arr_category.size], inv[arr_category.size:]
sorter_index = np.argsort(inv_map)
idx = np.searchsorted(inv_map, inv_arr, sorter=sorter_index)
store_name = arr_category['category_small_name'][sorter_index[idx]]
new_arr = make_array(arr_product.size, **{col: arr_product[col]
for col in arr_product.dtype.names},
store_name=store_name)
new_arr[:10]
array([('P040101001', '04', '0401', '040101', 198, 149, '弁当類'),
('P040101002', '04', '0401', '040101', 218, 164, '弁当類'),
('P040101003', '04', '0401', '040101', 230, 173, '弁当類'),
('P040101004', '04', '0401', '040101', 248, 186, '弁当類'),
('P040101005', '04', '0401', '040101', 268, 201, '弁当類'),
('P040101006', '04', '0401', '040101', 298, 224, '弁当類'),
('P040101007', '04', '0401', '040101', 338, 254, '弁当類'),
('P040101008', '04', '0401', '040101', 420, 315, '弁当類'),
('P040101009', '04', '0401', '040101', 498, 374, '弁当類'),
('P040101010', '04', '0401', '040101', 580, 435, '弁当類')],
dtype=[('product_cd', '<U10'), ('category_major_cd', '<U10'), ('category_medium_cd', '<U10'), ('category_small_cd', '<U10'), ('unit_price', '<i4'), ('unit_cost', '<i4'), ('store_name', '<U15')])
P_038
P-038: 顧客データフレーム(df_customer)とレシート明細データフレーム(df_receipt)から、各顧客ごとの売上金額合計を求めよ。ただし、買い物の実績がない顧客については売上金額を0として表示させること。また、顧客は性別コード(gender_cd)が女性(1)であるものを対象とし、非会員(顧客IDが'Z'から始まるもの)は除外すること。なお、結果は10件だけ表示させれば良い。
同様に、まず両フレームのキー列を結合した上で数値データに変換します。つづいてnp.bincount()
を利用して顧客ごとの合計を求めますが、このとき第三引数minlength
を用いると出力される行列が任意のサイズになりますのでunq.size
を指定します。最後に、数値化したキー列のレシート明細データ側をインデックスにして値を取得します。
is_member_receipt = arr_receipt['customer_id'].astype('<U1') != 'Z'
is_member_customer = ((arr_customer['customer_id'].astype('<U1') != 'Z')
& (arr_customer['gender_cd'] == 1))
customer = arr_customer['customer_id'][is_member_customer]
unq, inv = np.unique(
np.concatenate([customer, arr_receipt['customer_id'][is_member_receipt]]),
return_inverse=True)
customer_size = customer.size
amount_sum = np.bincount(
inv[customer_size:], arr_receipt['amount'][is_member_receipt], unq.size)
new_arr = make_array(customer_size,
customer_id=customer,
amount=amount_sum[inv[:customer_size]])
new_arr[:10]
array([('CS021313000114', 0.), ('CS031415000172', 5088.),
('CS028811000001', 0.), ('CS001215000145', 875.),
('CS015414000103', 3122.), ('CS033513000180', 868.),
('CS035614000014', 0.), ('CS011215000048', 3444.),
('CS009413000079', 0.), ('CS040412000191', 210.)],
dtype=[('customer_id', '<U14'), ('amount', '<f8')])
P_039
P-039: レシート明細データフレーム(df_receipt)から売上日数の多い顧客の上位20件と、売上金額合計の多い顧客の上位20件を抽出し、完全外部結合せよ。ただし、非会員(顧客IDが'Z'から始まるもの)は除外すること。
np.partition()
を用いて20位の値を取得し、上位20位に入っていない値をnp.nan
に置換後、インデックスを取得します。
is_member = arr_receipt['customer_id'].astype('<U1') != 'Z'
unq, inv = np.unique(arr_receipt['customer_id'][is_member],
return_inverse=True)
sums = np.bincount(inv, arr_receipt['amount'][is_member], unq.size)
is_sum_top = sums >= -np.partition(-sums, 20)[20]
sums[~is_sum_top] = np.nan
unq2 = np.unique([inv, arr_receipt['sales_ymd'][is_member]], axis=-1)
counts = np.bincount(unq2[0]).astype(float)
is_cnt_top = counts >= -np.partition(-counts, 20)[20]
counts[~is_cnt_top] = np.nan
interserction = is_cnt_top | is_sum_top
make_array(interserction.sum(),
customer_id=unq[interserction],
amount=sums[interserction],
sales_ymd=counts[interserction])
array([('CS001605000009', 18925., nan), ('CS006515000023', 18372., nan),
('CS007514000094', 15735., nan), ('CS007515000107', nan, 18.),
('CS009414000059', 15492., nan), ('CS010214000002', nan, 21.),
('CS010214000010', 18585., 22.), ('CS011414000106', 18338., nan),
('CS011415000006', 16094., nan), ('CS014214000023', nan, 19.),
('CS014415000077', nan, 18.), ('CS015415000185', 20153., 22.),
('CS015515000034', 15300., nan), ('CS016415000101', 16348., nan),
('CS016415000141', 18372., 20.), ('CS017415000097', 23086., 20.),
('CS021514000045', nan, 19.), ('CS021515000056', nan, 18.),
('CS021515000089', 17580., nan), ('CS021515000172', nan, 19.),
('CS021515000211', nan, 18.), ('CS022515000028', nan, 18.),
('CS022515000226', nan, 19.), ('CS026414000059', 15153., nan),
('CS028415000007', 19127., 21.), ('CS030214000008', nan, 18.),
('CS030415000034', 15468., nan), ('CS031414000051', 19202., 19.),
('CS031414000073', nan, 18.), ('CS032414000072', 16563., nan),
('CS032415000209', nan, 18.), ('CS034415000047', 16083., nan),
('CS035414000024', 17615., nan), ('CS038415000104', 17847., nan),
('CS039414000052', nan, 19.), ('CS040214000008', nan, 23.)],
dtype=[('customer_id', '<U14'), ('amount', '<f8'), ('sales_ymd', '<f8')])
P_040
P-040: 全ての店舗と全ての商品を組み合わせると何件のデータとなるか調査したい。店舗(df_store)と商品(df_product)を直積した件数を計算せよ。
問題の意図がわかりません……。
arr_store.size * arr_product.size
531590
# 模範解答
%%timeit
df_store_tmp = df_store.copy()
df_product_tmp = df_product.copy()
df_store_tmp['key'] = 0
df_product_tmp['key'] = 0
len(pd.merge(df_store_tmp, df_product_tmp, how='outer', on='key'))
# 277 ms ± 6.09 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
# NumPy
%timeit arr_store.size * arr_product.size
# 136 ns ± 1.69 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)
P_041
P-041: レシート明細データフレーム(df_receipt)の売上金額(amount)を日付(sales_ymd)ごとに集計し、前日からの売上金額増減を計算せよ。なお、計算結果は10件表示すればよい。
配列における前後の差を取得するにはnp.ediff1d()
を用います。
unq, inv = np.unique(arr_receipt['sales_ymd'], return_inverse=True)
diff_amount = np.ediff1d(np.bincount(inv, arr_receipt['amount']))
make_array(unq.size, sales_ymd=unq,
diff_amount=np.concatenate([[np.nan], diff_amount]))[:10]
array([(20170101, nan), (20170102, -9558.), (20170103, 3338.),
(20170104, 8662.), (20170105, 1665.), (20170106, -5443.),
(20170107, -8972.), (20170108, 1322.), (20170109, 1981.),
(20170110, -6575.)],
dtype=[('sales_ymd', '<i4'), ('diff_amount', '<f8')])
P_042
P-042: レシート明細データフレーム(df_receipt)の売上金額(amount)を日付(sales_ymd)ごとに集計し、各日付のデータに対し、1日前、2日前、3日前のデータを結合せよ。結果は10件表示すればよい。
スライスで取ります。
unq, inv = np.unique(arr_receipt['sales_ymd'], return_inverse=True)
amount = np.bincount(inv, arr_receipt['amount'])
make_array(unq.size - 3,
sales_ymd=unq[3:], amount=amount[3:],
lag_ymd_1=unq[2:-1], lag_amount_1=amount[2:-1],
lag_ymd_2=unq[1:-2], lag_amount_2=amount[1:-2],
lag_ymd_3=unq[:-3], lag_amount_3=amount[:-3])[:10]
array([(20170104, 36165., 20170103, 27503., 20170102, 24165., 20170101, 33723.),
(20170105, 37830., 20170104, 36165., 20170103, 27503., 20170102, 24165.),
(20170106, 32387., 20170105, 37830., 20170104, 36165., 20170103, 27503.),
(20170107, 23415., 20170106, 32387., 20170105, 37830., 20170104, 36165.),
(20170108, 24737., 20170107, 23415., 20170106, 32387., 20170105, 37830.),
(20170109, 26718., 20170108, 24737., 20170107, 23415., 20170106, 32387.),
(20170110, 20143., 20170109, 26718., 20170108, 24737., 20170107, 23415.),
(20170111, 24287., 20170110, 20143., 20170109, 26718., 20170108, 24737.),
(20170112, 23526., 20170111, 24287., 20170110, 20143., 20170109, 26718.),
(20170113, 28004., 20170112, 23526., 20170111, 24287., 20170110, 20143.)],
dtype=[('sales_ymd', '<i4'), ('amount', '<f8'), ('lag_ymd_1', '<i4'), ('lag_amount_1', '<f8'), ('lag_ymd_2', '<i4'), ('lag_amount_2', '<f8'), ('lag_ymd_3', '<i4'), ('lag_amount_3', '<f8')])
P_043
P-043: レシート明細データフレーム(df_receipt)と顧客データフレーム(df_customer)を結合し、性別(gender)と年代(ageから計算)ごとに売上金額(amount)を合計した売上サマリデータフレーム(df_sales_summary)を作成せよ。性別は0が男性、1が女性、9が不明を表すものとする。
ただし、項目構成は年代、女性の売上金額、男性の売上金額、性別不明の売上金額の4項目とすること(縦に年代、横に性別のクロス集計)。また、年代は10歳ごとの階級とすること。
まず、両フレームのキー列を結合した上で数値データに変換します。つづいて、欠損値で埋めた行列map_array
を作成し、顧客データ側の数値化した顧客IDをインデックスにして年代・性別を行列に入れ込みます。その後、レシート明細データ側の数値化した顧客IDをインデックスにして、年代・性別を取得します。最後に、年代・性別の二次元平面を作成し、性別・年齢をインデックスにして売上金額を足していきます。
# 顧客IDを数値データに変換
unq, inv = np.unique(np.concatenate([arr_customer['customer_id'],
arr_receipt['customer_id']]),
return_inverse=True)
inv_map, inv_arr = inv[:arr_customer.size], inv[arr_customer.size:]
# 年代の取得(欠損値=0)
map_array = np.zeros(unq.size, dtype=int)
map_array[inv_map] = arr_customer['age']//10
arr_age = map_array[inv_arr]
max_age = arr_age.max()+1
# 性別の取得(欠損値=9)
# map_array = np.full(unq.size, 9, dtype=int)
map_array[:] = 9
map_array[inv_map] = arr_customer['gender_cd']
arr_gender = map_array[inv_arr]
# 年代・性別の二次元平面上に売上を合計
arr_sales_summary = np.zeros((max_age, arr_gender.max()+1), dtype=int)
np.add.at(arr_sales_summary, (arr_age, arr_gender), arr_receipt['amount'])
# 構造化配列に変換
make_array(max_age,
era=np.arange(max_age)*10,
male=arr_sales_summary[:, 0],
female=arr_sales_summary[:, 1],
unknown=arr_sales_summary[:, 9])
array([( 0, 0, 0, 12395003), (10, 1591, 149836, 4317),
(20, 72940, 1363724, 44328), (30, 177322, 693047, 50441),
(40, 19355, 9320791, 483512), (50, 54320, 6685192, 342923),
(60, 272469, 987741, 71418), (70, 13435, 29764, 2427),
(80, 46360, 262923, 5111), (90, 0, 6260, 0)],
dtype=[('era', '<i4'), ('male', '<i4'), ('female', '<i4'), ('unknown', '<i4')])
模範解答:177 ms ± 3.45 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
NumPy:66.4 ms ± 1.28 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
P_044
P-044: 前設問で作成した売上サマリデータフレーム(df_sales_summary)は性別の売上を横持ちさせたものであった。このデータフレームから性別を縦持ちさせ、年代、性別コード、売上金額の3項目に変換せよ。ただし、性別コードは男性を'00'、女性を'01'、不明を'99'とする。
arr_amount = arr_sales_summary[:, [0, 1, 9]].ravel()
make_array(arr_amount.size,
era=(np.arange(max_age)*10).repeat(3),
gender_cd=np.tile(np.array(['00', '01', '99']), max_age),
amount=arr_amount)
array([( 0, '00', 0), ( 0, '01', 0), ( 0, '99', 12395003),
(10, '00', 1591), (10, '01', 149836), (10, '99', 4317),
(20, '00', 72940), (20, '01', 1363724), (20, '99', 44328),
(30, '00', 177322), (30, '01', 693047), (30, '99', 50441),
(40, '00', 19355), (40, '01', 9320791), (40, '99', 483512),
(50, '00', 54320), (50, '01', 6685192), (50, '99', 342923),
(60, '00', 272469), (60, '01', 987741), (60, '99', 71418),
(70, '00', 13435), (70, '01', 29764), (70, '99', 2427),
(80, '00', 46360), (80, '01', 262923), (80, '99', 5111),
(90, '00', 0), (90, '01', 6260), (90, '99', 0)],
dtype=[('era', '<i4'), ('gender_cd', '<U2'), ('amount', '<i4')])