だから僕はpandasを辞めた【データサイエンス100本ノック(構造化データ加工編)篇 #1】
データサイエンス100本ノック(構造化データ加工編)のPythonの問題を解いていきます。この問題群は、模範解答ではpandasを使ってデータ加工を行っていますが、私達は勉強がてらにNumPyの構造化配列を用いて処理していきます。
はじめに
Pythonでデータサイエンス的なことをする人の多くはpandas大好き人間かもしれませんが、実はpandasを使わなくても、NumPyで同じことができます。そしてNumPyの方がたいてい高速です。
pandas大好き人間だった僕もNumPyの操作には依然として慣れていないので、今回この『データサイエンス100本ノック』をNumPyで操作することでpandasからの卒業を試みて行きたいと思います。
今回は8問目までをやっていきます。
今回使うのはreceipt.csv
だけみたいです。初期データは以下のようにして読み込みました(データ型指定はとりあえず後回し)。
import numpy as np
import pandas as pd
# 模範解答用
df_receipt = pd.read_csv('data/receipt.csv')
# 僕たちが扱うデータ
arr_receipt = np.genfromtxt(
'data/receipt.csv', delimiter=',', encoding='utf-8',
names=True, dtype=None)
省メモリです。
import sys
sys.getsizeof(df_receipt)
# 26065721
sys.getsizeof(arr_receipt)
# 15074160
P_001
P-001: レシート明細のデータフレーム(df_receipt)から全項目の先頭10件を表示し、どのようなデータを保有しているか目視で確認せよ。
スライスで取ります。
arr_receipt[: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')])
模範解答(pandas)と速度を比較してみます。
# 模範解答
%timeit df_receipt.head(10)
# 130 µs ± 5.02 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)
%timeit arr_receipt[:10]
# 244 ns ± 8.23 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
模範解答の 1/500 の速さで取得できました。
P_002
P-002: レシート明細のデータフレーム(df_receipt)から売上日(sales_ymd)、顧客ID(customer_id)、商品コード(product_cd)、売上金額(amount)の順に列を指定し、10件表示させよ。
NumPyの構造化配列は、私達が慣れ親しんだpd.DataFrame
と同じように操作できます。
arr_receipt[['sales_ymd', 'customer_id', 'product_cd', 'amount']][:10]
array([(20181103, 'CS006214000001', 'P070305012', 158),
(20181118, 'CS008415000097', 'P070701017', 81),
(20170712, 'CS028414000014', 'P060101005', 170),
(20190205, 'ZZ000000000000', 'P050301001', 25),
(20180821, 'CS025415000050', 'P060102007', 90),
(20190605, 'CS003515000195', 'P050102002', 138),
(20181205, 'CS024514000042', 'P080101005', 30),
(20190922, 'CS040415000178', 'P070501004', 128),
(20170504, 'ZZ000000000000', 'P071302010', 770),
(20191010, 'CS027514000015', 'P071101003', 680)],
dtype={'names':['sales_ymd','customer_id','product_cd','amount'], 'formats':['<i4','<U14','<U10','<i4'], 'offsets':[0,40,96,140], 'itemsize':144})
pd.DataFrame
を操作するよりも高速です。
%timeit df_receipt[['sales_ymd', 'customer_id', 'product_cd', 'amount']].head(10)
# 5.19 ms ± 43.5 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
%timeit arr_receipt[['sales_ymd', 'customer_id', 'product_cd', 'amount']][:10]
# 906 ns ± 17.5 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
P_003
P-003: レシート明細のデータフレーム(df_receipt)から売上日(sales_ymd)、顧客ID(customer_id)、商品コード(product_cd)、売上金額(amount)の順に列を指定し、10件表示させよ。ただし、sales_ymdはsales_dateに項目名を変更しながら抽出すること。
名前の変更は地味に面倒臭いことです。np.lib.recfunctions.rename_fields()
を介すのが一番簡単ですが、それでもこの関数は結構使い勝手が悪かったりします。
np.lib.recfunctions.rename_fields(arr_receipt, {'sales_ymd': 'sales_date'})[
['sales_date', 'customer_id', 'product_cd', 'amount']][:10]
array([(20181103, 'CS006214000001', 'P070305012', 158),
(20181118, 'CS008415000097', 'P070701017', 81),
(20170712, 'CS028414000014', 'P060101005', 170),
(20190205, 'ZZ000000000000', 'P050301001', 25),
(20180821, 'CS025415000050', 'P060102007', 90),
(20190605, 'CS003515000195', 'P050102002', 138),
(20181205, 'CS024514000042', 'P080101005', 30),
(20190922, 'CS040415000178', 'P070501004', 128),
(20170504, 'ZZ000000000000', 'P071302010', 770),
(20191010, 'CS027514000015', 'P071101003', 680)],
dtype={'names':['sales_date','customer_id','product_cd','amount'], 'formats':['<i4','<U14','<U10','<i4'], 'offsets':[0,40,96,140], 'itemsize':144})
とはいえ、pandasに比べてとても高速に処理してくれます。
%timeit df_receipt[['sales_ymd', 'customer_id', 'product_cd', 'amount']].rename(columns={'sales_ymd': 'sales_date'}).head(10)
# 12.8 ms ± 252 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
%timeit np.lib.recfunctions.rename_fields(arr_receipt, {'sales_ymd': 'sales_date'})[['sales_date', 'customer_id', 'product_cd', 'amount']][:10]
# 6.19 µs ± 132 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
pandasは処理のたびに新しいデータフレームオブジェクトをどんどこ作っているので遅いかつ重いわけです。
P_004
P-004: レシート明細のデータフレーム(df_receipt)から売上日(sales_ymd)、顧客ID(customer_id)、商品コード(product_cd)、売上金額(amount)の順に列を指定し、以下の条件を満たすデータを抽出せよ。
- 顧客ID(customer_id)が"CS018205000001"
これも直感的操作で取得できます。
arr_receipt[['sales_ymd', 'customer_id', 'product_cd', 'amount']][
arr_receipt['customer_id'] == 'CS018205000001']
arr_receipt[['sales_ymd','customer_id','product_cd','amount']][arr_receipt['customer_id'] == 'CS018205000001']
array([(20180911, 'CS018205000001', 'P071401012', 2200),
(20180414, 'CS018205000001', 'P060104007', 600),
(20170614, 'CS018205000001', 'P050206001', 990),
(20170614, 'CS018205000001', 'P060702015', 108),
(20190216, 'CS018205000001', 'P071005024', 102),
(20180414, 'CS018205000001', 'P071101002', 278),
(20190226, 'CS018205000001', 'P070902035', 168),
(20190924, 'CS018205000001', 'P060805001', 495),
(20190226, 'CS018205000001', 'P071401020', 2200),
(20180911, 'CS018205000001', 'P071401005', 1100),
(20190216, 'CS018205000001', 'P040101002', 218),
(20190924, 'CS018205000001', 'P091503001', 280)],
dtype={'names':['sales_ymd','customer_id','product_cd','amount'], 'formats':['<i4','<U14','<U10','<i4'], 'offsets':[0,40,96,140], 'itemsize':144})
模範解答はpd.DataFrame.query()
が大好きのようです。普通にインデックスすればいいと思うんですけどね。
%timeit df_receipt[['sales_ymd', 'customer_id', 'product_cd', 'amount']].query('customer_id == "CS018205000001"')
# 11.6 ms ± 477 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
%timeit df_receipt.loc[df_receipt['customer_id'] == 'CS018205000001', ['sales_ymd', 'customer_id', 'product_cd', 'amount']]
# 9.49 ms ± 212 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
%timeit arr_receipt[['sales_ymd', 'customer_id', 'product_cd', 'amount']][arr_receipt['customer_id'] == 'CS018205000001']
# 2.7 ms ± 475 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
P_005
P-005: レシート明細のデータフレーム(df_receipt)から売上日(sales_ymd)、顧客ID(customer_id)、商品コード(product_cd)、売上金額(amount)の順に列を指定し、以下の条件を満たすデータを抽出せよ。
- 顧客ID(customer_id)が"CS018205000001"
- 売上金額(amount)が1,000以上
条件が複数あっても同じです。
arr_receipt[['sales_ymd', 'customer_id', 'product_cd', 'amount']][
(arr_receipt['customer_id'] == 'CS018205000001') & (arr_receipt['amount'] >= 1000)]
array([(20180911, 'CS018205000001', 'P071401012', 2200),
(20190226, 'CS018205000001', 'P071401020', 2200),
(20180911, 'CS018205000001', 'P071401005', 1100)],
dtype={'names':['sales_ymd','customer_id','product_cd','amount'], 'formats':['<i4','<U14','<U10','<i4'], 'offsets':[0,40,96,140], 'itemsize':144})
P_006
P-006: レシート明細データフレーム「df_receipt」から売上日(sales_ymd)、顧客ID(customer_id)、商品コード(product_cd)、売上数量(quantity)、売上金額(amount)の順に列を指定し、以下の条件を満たすデータを抽出せよ。
- 顧客ID(customer_id)が"CS018205000001"
- 売上金額(amount)が1,000以上または売上数量(quantity)が5以上
なんだか行が長くなってきたので分けます。
col_list = ['sales_ymd', 'customer_id', 'product_cd', 'quantity', 'amount']
cond = ((arr_receipt['customer_id'] == 'CS018205000001')
& ((arr_receipt['amount'] >= 1000) | (arr_receipt['quantity'] >= 5)))
arr_receipt[col_list][cond]
array([(20180911, 'CS018205000001', 'P071401012', 2200),
(20180414, 'CS018205000001', 'P060104007', 600),
(20170614, 'CS018205000001', 'P050206001', 990),
(20190226, 'CS018205000001', 'P071401020', 2200),
(20180911, 'CS018205000001', 'P071401005', 1100)],
dtype={'names':['sales_ymd','customer_id','product_cd','amount'], 'formats':['<i4','<U14','<U10','<i4'], 'offsets':[0,40,96,140], 'itemsize':144})
P_007
P-007: レシート明細のデータフレーム(df_receipt)から売上日(sales_ymd)、顧客ID(customer_id)、商品コード(product_cd)、売上金額(amount)の順に列を指定し、以下の条件を満たすデータを抽出せよ。
- 顧客ID(customer_id)が"CS018205000001"
- 売上金額(amount)が1,000以上2,000以下
col_list = ['sales_ymd', 'customer_id', 'product_cd', 'quantity', 'amount']
cond = ((arr_receipt['customer_id'] == 'CS018205000001')
& ((1000 <= arr_receipt['amount']) & (arr_receipt['amount'] <= 2000)))
arr_receipt[col_list][cond]
array([(20180911, 'CS018205000001', 'P071401005', 1, 1100)],
dtype={'names':['sales_ymd','customer_id','product_cd','quantity','amount'], 'formats':['<i4','<U14','<U10','<i4','<i4'], 'offsets':[0,40,96,136,140], 'itemsize':144})
P_008
P-008: レシート明細のデータフレーム(df_receipt)から売上日(sales_ymd)、顧客ID(customer_id)、商品コード(product_cd)、売上金額(amount)の順に列を指定し、以下の条件を満たすデータを抽出せよ。
- 顧客ID(customer_id)が"CS018205000001"
- 商品コード(product_cd)が"P071401019"以外
col_list = ['sales_ymd', 'customer_id', 'product_cd', 'quantity', 'amount']
cond = ((arr_receipt['customer_id'] == 'CS018205000001')
& (arr_receipt['product_cd'] != 'P071401019'))
arr_receipt[col_list][cond]
array([(20180911, 'CS018205000001', 'P071401012', 1, 2200),
(20180414, 'CS018205000001', 'P060104007', 6, 600),
(20170614, 'CS018205000001', 'P050206001', 5, 990),
(20170614, 'CS018205000001', 'P060702015', 1, 108),
(20190216, 'CS018205000001', 'P071005024', 1, 102),
(20180414, 'CS018205000001', 'P071101002', 1, 278),
(20190226, 'CS018205000001', 'P070902035', 1, 168),
(20190924, 'CS018205000001', 'P060805001', 1, 495),
(20190226, 'CS018205000001', 'P071401020', 1, 2200),
(20180911, 'CS018205000001', 'P071401005', 1, 1100),
(20190216, 'CS018205000001', 'P040101002', 1, 218),
(20190924, 'CS018205000001', 'P091503001', 1, 280)],
dtype={'names':['sales_ymd','customer_id','product_cd','quantity','amount'], 'formats':['<i4','<U14','<U10','<i4','<i4'], 'offsets':[0,40,96,136,140], 'itemsize':144})
col_list = ['sales_ymd', 'customer_id', 'product_cd', 'quantity', 'amount']
%timeit df_receipt[col_list].query('customer_id == "CS018205000001" & product_cd != "P071401019"')
# 15.6 ms ± 1.19 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
%timeit arr_receipt[col_list][((arr_receipt['customer_id'] == 'CS018205000001') & (arr_receipt['product_cd'] != 'P071401019'))]
# 4.28 ms ± 86.3 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
おわりに
ほいよ
これ、NumPyで作った高速・アチアチ・データ加工ね
処理後にcsvに吐き出してもいいし、pandasで重い処理することはないんだよ
だから僕はpandasをやめた