• Coupon Purchase Predictionは、過去の購入と閲覧の行動から、与えられた期間内に顧客がどのクーポンを購入するかを問うコンペです。
  • このコンペのデータをHoloViews(+GeoViews)で可視化してみます。
  • 初心者なので、可視化の観点などご指南いただけると嬉しいです。

環境

  • Jupyter Notebook 5.4.1
  • Python 3.6.4
  • HoloViews 1.9.5
  • GeoViews 1.4.3

Notebookの準備

モジュールのインポート

import numpy as np
import pandas as pd
import holoviews as hv
import geoviews as gv
import geoviews.feature as gf
import cartopy
import cartopy.feature as cf
from cartopy import crs as ccrs
hv.extension('bokeh')
  • バックエンドをBokehにしているのでぬるぬる動きます。

データの読み込み

prefecture_locations = pd.read_csv('../input/prefecture_locations.csv')
coupon_area_test = pd.read_csv('../input/coupon_area_test.csv')
coupon_area_train = pd.read_csv('../input/coupon_area_train.csv')
coupon_detail_train = pd.read_csv('../input/coupon_detail_train.csv')
coupon_list_test = pd.read_csv('../input/coupon_list_test.csv')
coupon_list_train = pd.read_csv('../input/coupon_list_train.csv')
coupon_visit_train = pd.read_csv('../input/coupon_visit_train.csv')
sample_submission = pd.read_csv('../input/sample_submission.csv')
user_list = pd.read_csv('../input/user_list.csv')
  • データの詳細についてはこちらをご覧ください。

性・年齢階級別のユーザ数

%%opts Bars (cmap='Category10') [width=960 height=600 tools=['hover'] group_index='SEX_ID']
d = user_list
d['AGE_round'] = d['AGE'].map(lambda x : int(x / 10) * 10)
d = d.groupby(['AGE_round', 'SEX_ID']).size()
hv.Bars((d.index.get_level_values('AGE_round'), d.index.get_level_values('SEX_ID'), d), 
        kdims=[('AGE_round', 'Age'), ('SEX_ID', 'Sex')], 
        vdims='User Count')

bokeh_plot.png

  • 30代女性が最も多いようです。

性・年齢階級別のクーポン購入数

%%opts Bars (cmap='Category10') [width=960 height=600 tools=['hover'] group_index='SEX_ID']
d = user_list
d['AGE_text'] = d['AGE'].map(lambda x : int(x / 10) * 10)
d = d.join(coupon_detail_train.set_index('USER_ID_hash'), on='USER_ID_hash')
d = d.groupby(['AGE_text', 'SEX_ID']).count()['USER_ID_hash']
hv.Bars((d.index.get_level_values('AGE_text'), d.index.get_level_values('SEX_ID'), d), 
        kdims=[('AGE_text', 'Age'), ('SEX_ID', 'Sex')], 
        vdims='Purchased Coupon Count')

bokeh_plot.png

  • 40代女性の購入数が最も多いようです。

性・年齢階級別の1人あたりの利用金額

%%opts Bars (cmap='Category10') [width=960 height=600 tools=['hover'] group_index='SEX_ID']
d = user_list
d['AGE_text'] = d['AGE'].map(lambda x : int(x / 10) * 10)
d = d.join(coupon_detail_train.set_index('USER_ID_hash'), on='USER_ID_hash').join(coupon_list_train.set_index('COUPON_ID_hash'), on='COUPON_ID_hash')
d = d.groupby(['AGE_text', 'SEX_ID']).sum()['DISCOUNT_PRICE'] / user_list.groupby(['AGE_text', 'SEX_ID']).size()
hv.Bars((d.index.get_level_values('AGE_text'), d.index.get_level_values('SEX_ID'), d), 
        kdims=[('AGE_text', 'Age'), ('SEX_ID', 'Sex')], 
        vdims='Purchased Amount')

bokeh_plot.png

  • 60代女性の利用金額が最も多いようです。

ジャンル別のクーポン数

%%opts Bars (cmap='Spectral') [show_legend=False width=960 height=600 tools=['hover'] stack_index='CAPSULE_TEXT']
d = coupon_list_train.groupby(['GENRE_NAME', 'CAPSULE_TEXT']).size()
hv.Bars((d.index.get_level_values('GENRE_NAME'), d.index.get_level_values('CAPSULE_TEXT'), d), 
        kdims=[('GENRE_NAME', 'Genre'), ('CAPSULE_TEXT', 'Capsule')], 
        vdims='Coupon Count')

bokeh_plot.png

  • 宅配(通販?)のクーポンが最も多いようです。

ジャンル別の1クーポンあたりの購入数

%%opts Bars (cmap='Spectral') [show_legend=False width=960 height=600 tools=['hover'] stack_index='CAPSULE_TEXT']
d = coupon_detail_train.join(coupon_list_train.set_index('COUPON_ID_hash'), on='COUPON_ID_hash').groupby(['GENRE_NAME', 'CAPSULE_TEXT']).count()['COUPON_ID_hash'] / coupon_list_train.groupby(['GENRE_NAME', 'CAPSULE_TEXT']).size()
hv.Bars((d.index.get_level_values('GENRE_NAME'), d.index.get_level_values('CAPSULE_TEXT'), d), 
        kdims=[('GENRE_NAME', 'Genre'), ('CAPSULE_TEXT', 'Capsule')], 
        vdims='Purchased Coupon Count')

bokeh_plot.png

  • ギフトカードが最も人気が高いようです。(金券だからでしょうか)

有効期間が未設定のクーポン

%%opts Bars (cmap='Spectral') [show_legend=False width=960 height=600 tools=['hover'] stack_index='CAPSULE_TEXT']
d = coupon_list_train
d = d[d['VALIDFROM'].isna()].groupby(['GENRE_NAME', 'CAPSULE_TEXT']).size()
hv.Bars((d.index.get_level_values('GENRE_NAME'), d.index.get_level_values('CAPSULE_TEXT'), d), [('GENRE_NAME', 'Genre'), ('CAPSULE_TEXT', 'Capsule')], 'Coupon Count', label='What is Validity?')

bokeh_plot.png

  • 宅配はクーポン購入と同時に発送されるということでしょうか。

ジャンル別の定価の分布

%%opts BoxWhisker (cmap='Spectral') [width=960 height=600 tools=['hover']]
d = coupon_list_train
hv.BoxWhisker((d['GENRE_NAME'], d['CATALOG_PRICE']), 'Genre', 'Catalog Price')

bokeh_plot.png

  • 68万円のレッスンがありますね…。

ジャンル別の割引後価格の分布

%%opts BoxWhisker (cmap='Spectral') [width=960 height=600 tools=['hover']]
d = coupon_list_train
hv.BoxWhisker((d['GENRE_NAME'], d['DISCOUNT_PRICE']), 'Genre', 'Discount Price')

bokeh_plot.png

  • 割引後は10万円まで下がってますね。ちなみに68万円のレッスンは東京都にお住いの42歳女性に購入されていました。

ジャンル別の割引率

%%opts BoxWhisker (cmap='Spectral') [width=960 height=600 tools=['hover']]
d = coupon_list_train
hv.BoxWhisker((d['GENRE_NAME'], d['PRICE_RATE']), 'Genre', 'Discount Rate')

bokeh_plot.png

  • だいたい5割以上割引されてますね。

性・年齢階級・ジャンル別のクーポン購入数

%%opts Bars (cmap='Spectral') [width=960 height=600 tools=['hover'] group_index='GENRE_NAME']
d = user_list
d['AGE_text'] = d['AGE'].map(lambda x : int(x / 10) * 10)
d = d.join(coupon_detail_train.set_index('USER_ID_hash'), on='USER_ID_hash').join(coupon_list_train.set_index('COUPON_ID_hash'), on='COUPON_ID_hash')
d_f = d[d['SEX_ID'] == 'f']
d_m = d[d['SEX_ID'] == 'm']
d_f = d_f.groupby(['AGE_text', 'GENRE_NAME']).count()['COUPON_ID_hash'].sort_index(level='GENRE_NAME')
d_m = d_m.groupby(['AGE_text', 'GENRE_NAME']).count()['COUPON_ID_hash'].sort_index(level='GENRE_NAME')
hv.Bars((d_f.index.get_level_values('AGE_text'), d_f.index.get_level_values('GENRE_NAME'), d_f), 
        kdims=[('AGE_text', 'Age'), ('GENRE_NAME', 'Genre')], 
        vdims='Purchased Coupon Count', label='Female') + \
hv.Bars((d_m.index.get_level_values('AGE_text'), d_m.index.get_level_values('GENRE_NAME'), d_m), 
        kdims=[('AGE_text', 'Age'), ('GENRE_NAME', 'Genre')], 
        vdims='Purchased Coupon Count', label='Male')

スクリーンショット 2018-04-09 17.46.21.png

  • +でグラフを並べられます。
  • グラフが複数あっても1つ目しかダウンロードできないようなのでここはスクリーンショットです。

都道府県別のクーポン購入数

%%opts Overlay [width=960 height=600] 
%%opts Points (size=0.5 cmap='Viridis' alpha=0.5) [tools=['hover'] size_index='COUPON_ID_hash' color_index='COUPON_ID_hash' colorbar=True xaxis=None yaxis=None show_legend=False]
d = coupon_detail_train.join(coupon_list_train.set_index('COUPON_ID_hash'), on='COUPON_ID_hash')
d_delivery = d[d['GENRE_NAME'] == '宅配']
d_other = d[d['GENRE_NAME'] != '宅配']
d = d.groupby('ken_name').count()
d = prefecture_locations.join(d, on='PREF_NAME')[['LONGITUDE', 'LATITUDE', 'COUPON_ID_hash', 'PREF_NAME']]
d_delivery = d_delivery.groupby('ken_name').count()
d_delivery = prefecture_locations.join(d_delivery, on='PREF_NAME')[['LONGITUDE', 'LATITUDE', 'COUPON_ID_hash', 'PREF_NAME']]
d_other = d_other.groupby('ken_name').count()
d_other = prefecture_locations.join(d_other, on='PREF_NAME')[['LONGITUDE', 'LATITUDE', 'COUPON_ID_hash', 'PREF_NAME']]
gv.WMTS('http://c.tile.openstreetmap.org/{Z}/{X}/{Y}.png') * \
gv.Dataset(d).to(gv.Points, 
        kdims=['LONGITUDE', 'LATITUDE'], 
        vdims=['COUPON_ID_hash', 'PREF_NAME'], 
        crs=ccrs.PlateCarree(), 
        label='All Genre') + \
gv.WMTS('http://c.tile.openstreetmap.org/{Z}/{X}/{Y}.png') * \
gv.Dataset(d_delivery).to(gv.Points, 
        kdims=['LONGITUDE', 'LATITUDE'], 
        vdims=['COUPON_ID_hash', 'PREF_NAME'], 
        crs=ccrs.PlateCarree(), 
        label='Delivery Service') +\
gv.WMTS('http://c.tile.openstreetmap.org/{Z}/{X}/{Y}.png') * \
gv.Dataset(d_other).to(gv.Points, 
        kdims=['LONGITUDE', 'LATITUDE'], 
        vdims=['COUPON_ID_hash', 'PREF_NAME'], 
        crs=ccrs.PlateCarree(), 
        label='Other Genre')

スクリーンショット 2018-04-09 17.57.10.png

  • ここだけGeoViewsを使っています。
  • 1つ目は全ジャンル、2つ目は宅配のみ、3つ目は宅配以外のグラフです。
  • 都道府県はユーザの居住地ではなく、クーポンが使える店舗の所在地です。
  • ちょっとわかりにくいですが、青森・秋田などは宅配の方が円が大きく、地元のユーザよりも宅配で成り立っていることがわかります。
Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account log in.