LoginSignup
1
6

More than 3 years have passed since last update.

だから僕はpandasを辞めた【データサイエンス100本ノック(構造化データ加工編)篇 #5】

Last updated at Posted at 2020-07-08

だから僕はpandasを辞めた【データサイエンス100本ノック(構造化データ加工編)篇 #5】

データサイエンス100本ノック(構造化データ加工編)のPythonの問題を解いていきます。この問題群は、模範解答ではpandasを使ってデータ加工を行っていますが、私達は勉強がてらにNumPyを用いて処理していきます。

:arrow_up:初回記事(#1)
:arrow_backward:前回記事(#4)
:arrow_forward:次回記事(#6)

はじめに

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()を使ってレシート明細の店舗コード列を置換していきます。

In[023]
_, 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]
Out[023]
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件表示させよ。

同じ。

In[037]
_, 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]
Out[037]
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を指定します。最後に、数値化したキー列のレシート明細データ側をインデックスにして値を取得します。

In[038]
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]
Out[038]
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に置換後、インデックスを取得します。

In[039]
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])
Out[039]
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)を直積した件数を計算せよ。

問題の意図がわかりません……。

In[040]
arr_store.size * arr_product.size
Out[040]
531590
Time[040]
# 模範解答
%%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()を用います。

In[041]
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]
Out[041]
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件表示すればよい。

スライスで取ります。

In[042]
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]
Out[042]
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をインデックスにして、年代・性別を取得します。最後に、年代・性別の二次元平面を作成し、性別・年齢をインデックスにして売上金額を足していきます。

In[043]
# 顧客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])
Out[043]
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')])
Time[043]
模範解答: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'とする。

In[044]
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)
Out[044]
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')])
1
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
6