ysaz (ImanazaS) blog

最近はデータ分析や機械学習が中心。たまに音楽や書評。

Pythonを使ったRFM分析

今回はこれまでと趣向を変えて、サンプルデータを使った分析手法(RFM分析)について取り上げる。

RFM分析は、Recency(直近)、Frequency(頻度)、Monetary(購入額)の略であり、マーケティングの分野において、顧客をグループ化した上で優良顧客を抽出し、確度の高い施策を講じる際に用いられる。

今回サンプルデータとして、以下にアップされているものを利用した。
RFM-analysis/sample-orders.csv at master · joaolcorreia/RFM-analysis · GitHub

import pandas as pd
import matplotlib.pyplot
import datetime as dt

# ファイルの読み込み
df = pd.read_csv('sample-orders.csv',sep=',')
df['order_date'] = pd.to_datetime(df['order_date'])
df.head()

#   order_date        order_id          customer  grand_total
# 0 2011-09-07  CA-2011-100006       Dennis Kane          378
# 1 2011-07-08  CA-2011-100090        Ed Braxton          699
# 2 2011-03-14  CA-2011-100293  Neil Franz�sisch           91
# 3 2011-01-29  CA-2011-100328   Jasper Cacioppo            4
# 4 2011-04-08  CA-2011-100363       Jim Mitchum           21

NOW = dt.datetime(2014,12,31)

# 顧客名を軸にデータをグループ化し、R、F、Mの3つの列を作成
rfmTable = df.groupby('customer').agg({'order_date': lambda x: (NOW - x.max()).days, # Recency
                                        'order_id': lambda x: len(x),      # Frequency
                                        'grand_total': lambda x: x.sum()}) # Monetary Value

rfmTable['order_date'] = rfmTable['order_date'].astype(int)
rfmTable.rename(columns={'order_date': 'recency', 
              'order_id': 'frequency', 
              'grand_total': 'monetary_value'}, inplace=True)
rfmTable.head()

#                  frequency  recency  monetary_value
# customer                                           
# Aaron Bergman            3      415             887
# Aaron Hawkins            7       12            1744
# Aaron Smayling           7       88            3050
# Adam Bellavance          8       54            7756
# Adam Hart               10       34            3249

# R、F、Mのそれぞれについて4段階でスコア付けをする関数を定義
def pct_rank_qcut(series, n):
    edges = pd.Series([float(i) / n for i in range(n + 1)])
    f = lambda x: (edges >= x).argmax()
    return series.rank(pct=1).apply(f)

# Recency
rfmTable['rec_dec'] = pct_rank_qcut(rfmTable['recency'], 4)

# Frequency(数が大きなものを1、小さなものを4とランク付け)
freq_dec = pct_rank_qcut(rfmTable['frequency'], 4)
rfmTable['freq_dec'] = 5 - freq_dec

# Monetary(数が大きなものを1、小さなものを4とランク付け)
mv_dec = pct_rank_qcut(rfmTable['monetary_value'], 4)
rfmTable['mv_dec'] = 5 - mv_dec

# R、F、Mの3つを組合せて、RFMスコアを作成
rfmTable['RFMClass'] = rfmTable.rec_dec.map(str) + rfmTable.freq_dec.map(str) + rfmTable.mv_dec.map(str)
rfmTable.head()

#                  frequency  recency  monetary_value  rec_dec  freq_dec  \
# customer                                                                 
# Aaron Bergman            3      415             887        4         4   
# Aaron Hawkins            7       12            1744        1         2   
# Aaron Smayling           7       88            3050        3         2   
# Adam Bellavance          8       54            7756        2         2   
# Adam Hart               10       34            3249        2         1   

#                  mv_dec RFMClass  
# customer                          
# Aaron Bergman         4      444  
# Aaron Hawkins         3      123  
# Aaron Smayling        2      322  
# Adam Bellavance       1      221  
# Adam Hart             2      212  

# Recencyの4つのグループにつき、購入金額の平均を比較
rec_dec_m = rfmTable.groupby(['rec_dec'])['monetary_value'].mean()
rec_dec_m.plot(kind='bar')

# Frequencyの4つのグループにつき、購入金額の平均を比較
freq_dec_m = rfmTable.groupby(['freq_dec'])['monetary_value'].mean()
freq_dec_m.plot(kind='bar')

Recencyに対するチャート(縦軸:購入金額の平均)
f:id:nami3373:20160919233951p:plain

Frequencyに対するチャート(縦軸:購入金額の平均)
f:id:nami3373:20160919233956p:plain

Pandasを使った行列のセレクティングについて

pandasでのiloc[行, 列]を使った操作に関する備忘録。
特定のデータを行あるいは列から抜き出して表示したり、違う数値へ置き換えたりする方法について記す。

import numpy as np
import pandas as pd

# データフレームの作成
df = pd.DataFrame(data=
[('2016-04-01', 10.2, 60.3), 
('2016-04-02', 16.4, 71.7), 
('2016-04-03', 11.1, 65.6), 
('2016-04-04', 15.1, 73.2)])
df.columns = ['date', 'temp', 'humidity']
df
#          date  temp  humidity
# 0  2016-04-01  10.2      60.3
# 1  2016-04-02  16.4      71.7
# 2  2016-04-03  11.1      65.6
# 3  2016-04-04  15.1      73.2

# データフレームから2行目のデータを抜き出し
df.iloc[1, :]
# date        2016-04-02
# temp              16.4
# humidity          71.7
# Name: 1, dtype: object

# データフレームから上2行のデータのみを抜き出し
df.iloc[:2]
#          date  temp  humidity
# 0  2016-04-01  10.2      60.3
# 1  2016-04-02  16.4      71.7

# データフレームから2列目のデータ(気温)を抜き出し
df.iloc[:, 1]
# 0    10.2
# 1    16.4
# 2    11.1
# 3    15.1
# Name: temp, dtype: float64

# 指定した位置にあるデータを別のものに置き換える。
df.loc[df['date'] == '2016-04-01', 'temp'] = 12.3
df
#          date  temp  humidity
# 0  2016-04-01  12.3      60.3
# 1  2016-04-02  16.4      71.7
# 2  2016-04-03  11.1      65.6
# 3  2016-04-04  15.1      73.2

行列の選択においてloc, iloc, ixという3つのメソッドが存在するが、以下のページによるとその違いは、
python - pandas iloc vs ix vs loc explanation, how are they different? - Stack Overflow

  • loc: インデックスのラベル名に対応
  • iloc: インデックスの順序に対応
  • ix: 基本はlocのように振る舞うが、インデックスにラベル名が存在しない場合、ilocのように振る舞う

とのことらしい。

例えば、2列目のデータを抜き出す際に"iloc"を使用しているが、行(すなわちインデックスおよびラベル名)を指定してないため、"loc"は使えず、エラーが返される。また、同じ例で"ix"を使った場合、"loc"が使えないので"iloc"と同じ結果が得られることになる。


なお、今回のデータの選択処理については、以下サイトも参考にしている。
sinhrks.hatenablog.com

Pandasでの複数ファイル読み込み

指定したフォルダ内に格納されている複数のファイルを読み込み、データフレームを作成する方法についての備忘録。

# 必要なパッケージの読み込み
import pandas as pd
import numpy as np

from datetime import date, datetime, timedelta
import time
import sys
import glob
import errno

# ファイルに日付データが含まれている前提で、開始日と終了日を指定する。
# データフレーム作成時にインデックスとして使用。
start = datetime(2015, 6, 1)
end = datetime(2016, 5, 31)
rng = pd.date_range(start, end)

# 次に関数を定義。xはパス名のうち省略したい部分を差す。
# データフレーム作成時、読み込みたいファイル名から、
# ディレクトリと拡張子を省いたものをカラム名とする。
def into_dataframe(path, x):   
    files = glob.glob(path)   
    df = pd.DataFrame()
    col = []
    list_ = []
    for name in files:
        try:
            # -4を入れることで拡張子を省く
            col.append(name[x:-4])
            d = pd.read_csv(name, sep='\t')
            list_.append(d.ix[:, 1:])
        except IOError as exc:
            if exc.errno != errno.EISDIR:
                raise
    df = pd.concat(list_, axis=1)
    df.index = rng
    df.columns = col
    return df

# データを読み込むパス名およびxを指定
path = '/Users/xxx/Documents/files/*.tsv'
df = into_dataframe(path, 100)

このように、ある程度ファイルのフォーマットが揃っている必要はあるが、複数のファイルを1つのデータフレームにまとめたいときに便利。

Pandasでのデータ集計

pandasを使ったデータフレームの成形について。meltやpivot_tableの使い方に関する備忘録。

例:
都市ID毎の日別気温、湿度データが与えられているが、各列にデータがまとめられている(例えば、気温の列に全ての都市IDに紐づくデータが一纏めになっている)とき、都市ID毎の気温、湿度データを取り出し、別の列にまとめ直す。
pandasを用いた操作は、次の3つのステップで実行する。

ステップ1:meltを使って、気温、湿度データを一列(value)にまとめる。あるいは、複数列で持っている値を行持ちに展開する。
ステップ2:IDとvariable名をくっつけ、keyという列をつくる。
ステップ3:pivot_tableを使って、key列を横方向へ展開する。あるいは、複数行で持っている値を列持ちに変換する。

import pandas as pd
import datetime

# データフレームの作成
df = pd.DataFrame(data=[('2016-04-01', 'a', 10.2, 60.3), ('2016-04-02', 'a', 16.4, 71.7), ('2016-04-01', 'b', 11.1, 65.6), ('2016-04-02', 'b', 15.1, 73.2)])
df.columns = ['date', 'id', 'temp', 'humidity']
# 成形前(df)
#          date id  temp  humidity
# 0  2016-04-01  a  10.2      60.3
# 1  2016-04-02  a  16.4      71.7
# 2  2016-04-01  b  11.1      65.6
# 3  2016-04-02  b  15.1      73.2

# ステップ1
df1 = pd.melt(df, id_vars = ['date', 'id'], value_vars = ['temp', 'humidity'])

# ステップ2
df1['key'] = df1['id'].map(str) + '_' + df1['variable'].map(str)
df1 = df1.drop(['id', 'variable'], 1)
df1['date'] = pd.to_datetime(df1['date'])
# df1['date'] = [datetime.datetime.strptime(i, '%Y-%m-%d') for i in df1['date']]  少し回りくどい方法
df1.index = df1['date']

# ステップ3
df2 = df1.pivot_table(values='value',  index='date', columns='key')
# 成形後(df2)
# key          a_humidity  a_temp  b_humidity  b_temp
# date
# 2016-04-01        60.3    10.2        65.6    11.1
# 2016-04-02        71.7    16.4        73.2    15.1

Pandasでの時系列操作

python、主にpandasの基本的な使い方について、備忘録として記述していく。
まずはタイトルの通り、時系列操作について。

例1
あるデータフレームに年(Year)列と月(Month)列データが入っているとき、
この2つを年月として合わせて、データフレームのインデックスに割り当てる。

import pandas as pd
from datetime import date time
df.index = df.apply(lambda x: datetime(int(x.Year), int(x.Month), 1), axis=1)


例2
2012年1月から2016年12月まで、60ヶ月のリストデータを作成する。
以下の例は月ごとに区切られたものだが、freqに'D'を指定すると日ごと、'Y'を指定すると年ごとになる。

import pandas as pd
index = pd.date_range('2012-1', periods=60, freq='M')


例3
データフレームの'date'という列に日別の時系列データが入っているとき、
月曜日から順に0, 1, 2, と数字を割り当てたあと、5(土曜)、6(日曜)を休日として定義し、
'DayStatus'という列でラベリングする。

#曜日列を追加
df['DayOfWeek'] = df['date'].apply(lambda x: x.weekday())

#平日休日判定の関数定義
def day_status(x):
  if x <= 4: return 0	#平日
  else:      return 1	#休日

#平日休日判定本番
df['DayStatus'] = df['DayOfWeek'].apply(lambda x: day_status(x))

七月二十五日のこと(アイスランド)

ここからが北欧旅行記。朝9時にアイスランドのケプラビーク国際空港に到着。ニューヨークからは5時間半程度のフライト。機内で映画を楽しむ時間もないくらい。こんなにすぐアイスランドに行けるんやなと少し感動した。空港は噂に聞いたとおり小さめながらも近代的なつくり。シースルーの天井で光の入り具合もいい感じ。

 

f:id:nami3373:20140725050504j:plain
 
それから空港の外に出て、申し込んでいたツアー会社のバスを探す。しかしいくら探せど指定された場所に車は来ない。しびれをきらして電話をかけてみると、まだ空港へ向かっている途中とのこと。どうやら僕の乗っていた飛行機が早く到着したために出迎えのタイミングがずれてしまった模様。
 
しばし空港に待機して周囲を見渡してみるが、見事なほど何もない。視界に入るのはゴツゴツした溶岩だらけ。立っている木も少なく、緑といえば溶岩の上に茂っている苔くらい。これはとんでもないところに来てしまったな、というのがアイスランドに着いた時の第一印象。苔が生えていなければ、ここが火星だと言われても頷いてしまうくらい辺り一面なにもない景色。
 
f:id:nami3373:20140725062101j:plain
 
早く抜け出したかったが空港を離れて行くのは大手ツアー会社のバスばかりで、僕が申し込んだ小さなツアー会社のバスはなかなか来ない。しばらくしてようやく迎えが来たが、それがオンボロのバンとわかったときに変に納得してしまった。これがどこよりも安い所以かと。ちなみに今回利用したBustravelとかいうツアー会社はネットであれこれ調べていたときにたまたま見つけた会社で、空港からブルーラグーン、ブルーラグーンからレイキャビクまでの送迎、ゴールデン・サークル1日ツアー、レイキャビクから空港までの送迎がすべてついて1万円ちょいという、他と比べるとかなり割安な価格設定。Lonely Planetのもちゃんと載ってたし、観光業者としてちゃんと登録もされているので、決して怪しいところではないみたいです。
 
さて、そんなオンボロバンに揺られてまず向かったのはブルーラグーン。世界最大の屋外温浴施設だというのと、久しぶりにゆっくり足を伸ばして風呂に入れることに感動し、1時間半ほど入浴を楽しんだ。お湯は乳濁しており、その色もブルーラグーンの神秘的な雰囲気を盛り立てているように思う。入場料が高過ぎて最初は行くかどうか悩んでしまったが、アイスランドに行くなら外せない場所の一つやなと強く感じた。
 

f:id:nami3373:20140725085017j:plain

 

14時半くらいに迎えのバスが来てレイキャビク市内へ移動。40分くらいの移動だったが、道中、目の前に広がっていたのはゴツゴツした溶岩と曇りがかった空で、じっと目を凝らしているとだんだん不安になってくる、そんな光景だった。

 

それでもレイキャビク市内は結構都会で、今回滞在した宿のあるLaugavegur通りにはブティックや飲食店が多数固まっていて、たくさんの人で賑わっていた。ストリートアートも盛んらしく、街の至るところでグラフィティを目にしたのも興味深かった。カラフルな建築物も多く見られ、結構アーティスティックな街だという印象を受けた。

 

街をぶらつきながら、前々から行ってみたかった12 TonarというCDショップへ。なんでもコーヒーを飲みながら店内に置いてあるCDを自由に視聴できることでも有名。早速手当たり次第にかき集めたCDを手に店長に声をかけてから試聴機へ。思えばここ数年、こうして店で新しい音楽をDigることをほとんどしていなかったことにハッと気づいた。最近の音楽の聴き方といえば、そのときの気分に合わせて何も考えずにSpotifyYouTubeでオススメのプレイリストを垂れ流すというもので、新たに見つけたアーティストに感動して、じっくり一つの作品を聴きこむ経験が明らかに減ってきている。確かに音楽のジャンルも飽和してきてるし、なかなかビックリするような目新しい出会いには恵まれないかもしれないが、自分の知らなかったアイスランドの良質なアーティストを知ることができたし、この店で聴いたこと、見たこと、考えたことを通じて、忘れかけていたものを少しだけ取り戻せた気がする。

 

f:id:nami3373:20140725123249j:plain

 

その他、有名なハットルグリムス教会やハルパ・レイキャヴィク・コンサートホールなどを巡り、夕食にはビストロで魚料理を堪能した。

七月二十四日のこと(ボストン〜ニューヨーク)

この日は昼過ぎのフライトでボストンからニューヨークへ向かう予定になっていて、午前中観光するのもしんどいし、前日の疲れも残っていたため、遅めのチェックアウトを経て少し早めに空港へ向かうことにした。しかし、移動中に早くもアクシデント発生。突然携帯に飛び込んできたメールを見ると飛行機の出発が30分ほど遅れるとのこと。ニューヨークでの約7時間のトランジット時間を使ってMOMAに行こうと計画していたが、雲行きが怪しくなった。

 
11時過ぎに空港に着くと、さらに30分延びるとの案内が。他の便に振り返るという案もあったが、JFKに向かう便でそれより早いのがなかったのと、自分の席がエコノミーからファーストへグレードアップされていたことがあり、そのまま空港で待つことにした。そもそも、予定していた便でもMoMAの見学時間は30分〜1時間程度しか見ていなかったので、少しでも飛行機が遅れた時点で計画の一部が流れてしまうのが大きかった。
 
その後事態はさらに悪化し、昼食を食べながら待っているとさらに遅れが生じたとの情報が。結局ニューヨークへの到着は当初の予定より2時間も遅れてしまい、マンハッタンに着いたのは18時半過ぎ。それから1時間かけてGrand Centralまで移動して友達と合流し、21時前まで飯を食ったのち再度JFK空港へ向かった。
 
途中電車も遅れるというハプニングにも遭遇したが、無事空港に到着し、23時半の便でレイキャビクへと飛び立つことができた。