[Pandas — Part V] สร้าง Single Customer View ด้วย Pandas

Supalerk Pisitsupakarn
Data Cafe Thailand
Published in
5 min readMay 20, 2020
credit pic

ใน Part V เราจะลองมาทำ Single Customer View กันนะครับ ซึ่งอาจจะต้องใช้คำสั่งต่างๆใน Pandas เพื่อ Manipulate data พอสมควร หากท่านใดสนใจอยากจะลองเล่นกับ Data จริงของฝั่ง Retail หรืออยากลองทำตาม สามารถอ่านรายละเอียดของ Dataset ชุดนี้ และดาวน์โหลดได้จากลิงค์นี้ครับ Retail Case Study Data

เริ่มจากเรามี file มาทั้งหมด 3 files ดังนี้

  1. Customer.csv
  2. prod_cat_info.csv
  3. Transactions.csv
#Read files
df_cus = pd.read_csv('Customer.csv')
df_tran = pd.read_csv('Transactions.csv')
df_prod = pd.read_csv('prod_cat_info.csv')
รูปที่ 1 : ตัวอย่างข้อมูลแต่ละ df

ซึ่งหน้าชุดข้อมูลนี่จะมีลักษณะเป็นแบบ Star Schema ที่มี Transaction(df_tran) เป็น Fact Table และมี Customer(df_cus) กับ prod_cat_info(df_prod) เป็น Dimension Tables

Step 1 : Left join(merge) df_tran with df_prod

#Merge df between df_tran and df_prod (left join)
df_tran_prod = df_tran.merge(df_prod , how = 'left' , left_on = ['prod_cat_code','prod_subcat_code'] , right_on =['prod_cat_code' ,'prod_sub_cat_code'])
#Drop unnecessary columns
df_tran_prod.drop(columns = ['prod_subcat_code' , 'prod_cat_code' , 'prod_sub_cat_code'] , inplace = True)
#Displays first 5 rows
df_tran_prod.head()
รูปที่ 2 : ตัวอย่าง df หลังจากการใช้ df_tran left join กับ df_prod

Step 2 : Handle with returned product

จากรูปที่ 2 เราจะเห็นได้อย่างชัดเจนว่า มีบาง transactions ที่มีการขอคืนของ (มี Qty ติดลบ) เพราะฉะนั้น เราจะมาลองดูว่า transaction_id ใด ที่เราควรจะตัดออกจากการคำนวน

สมมุติฐานแรกที่แวปขึ้นมา วันที่ซื้อของกับวันที่คืน อาจจะไม่ใช่วันเดียวกัน

รูปที่ 3 : ลองทดสอบดูว่าใน 1 transaction_id มีหลายวัน และมีการคืนของจริงหรือไม่

จากรูปที่ 3 เราจะเห็นได้ว่าหากเราลอง groupby(by= ‘transaction_id’) และดูว่ามีจำนวนวันที่ไม่ซ้ำกันทั้งหมดกี่วัน พบว่ามีบาง transactions ที่มีวันมากกว่า 1 วัน จากนั้นก็เลยลองมาดูรายละเอียดด้านใน พบว่าใน transaction_id ที่ 32263938079 นั้นมี 4 rows เป็น transactions Footwear ของเด็ก วันที่ซื้อ หรือวันที่ 1/2/2013 และมีวันที่มาคืนของคือ 3/2/2013 , 5/2/2013 และ 8/2/2013 ซึ่งต้องขออภัยที่ผมไม่ทราบว่า เพราะเหตุใดถึงมีการคืนของมากกว่าซื้อ อาจจะจำเป็นที่จะต้องสอบถามผู้รู้หรือ business user หน้างาน อย่างไรก็ตาม transaction_id นี้ อาจจะไม่ปกตินัก ในกรณีนี้ผมจึงจะขอเก็บ transactions ทั้งหมดที่มียอดรวมของ total_amt มากกว่า 0 เท่านั้นซึ่งสามารถทำได้โดย

#Create temp df which contains sum total_amt for each transaction_id
temp_tran_id = df_tran_prod.groupby(by='transaction_id')[['total_amt']].sum()
#Filter only transaction_id which have total_amt > 0
filtered_tran_id = temp_tran_id[temp_tran_id['total_amt']> 0 ].index
#Displays filtered_tran_id
filtered_tran_id
รูปที่ 4 : แสดง transaction_id ที่มียอดรวมของ total_amt มากกว่า 0

จากนั้น Filter df_tran_prod ให้เหลือเพียง transaction_id ที่อยู่ใน filtered_tran_id

# Filter every transactions which have total_amt > 0
df_tran_prod = df_tran_prod[df_tran_prod['transaction_id'].isin(filter_tran_id)]
df_tran_prod.head()
รูปที่ 5 : ตัวอย่างข้อมูลของ df_tran_prod หลังจาก filter

Step 3 : Extract customer buying behaviors

จากข้อมูลใน df_tran_prod นี้ 1 cust_id มีหลาย transactions หรือลูกค้า 1 คน สามารถมาซื้อสินค้าได้หลายครั้ง และในหนึ่งครั้งก็อาจจะซื้อสินค้าหลายอย่าง ดังนั้น เราจะทำการยุบรวมข้อมูลหลายๆ transactions นั้นให้เหลือเพียง 1 transaction เท่านั้น หรือสรุปก็คือ 1 cust_id จะต้องมีเพียง 1 row

Feature ในใจที่อยากได้

  1. avg_visit_per_month
  2. avg_spend_per_month
  3. avg_spend_per_time
  4. store_type_ratio
  5. prod_cat_ratio

เริ่ม !!!!

จากข้อ 1,2 และ 3 ใช้ค่า avg หรือ mean สิ่งที่จะต้องระวังทุกๆครั้งที่จะใช้ ค่า mean นั่นก็คือค่า extreme values หรือพวก Outliers มันจะเป็นตัวดึงให้ค่า Mean ให้เอียงไปทางค่าพวกนั้น แต่ในกรณีนี้เพื่อให้ง่ายผมจะขอทำแบบไม่ตัดก่อน

#Change tran_date data type from object to datetime
df_tran_prod['tran_date'] = pd.to_datetime(df_tran_prod['tran_date'])
#Create new column which contains only year and month
df_tran_prod['month_year'] = df_tran_prod['tran_date'].apply(lambda x: x.strftime('%Y-%m'))
#Groupby cust_id and use suitable aggfunc to each columns
scv = df_tran_prod.groupby(by='cust_id').agg({'tran_date': 'min',
'month_year' : 'nunique',
'transaction_id':'nunique',
'total_amt':'sum'})
#Change columns name
scv.columns = ['first_date' , 'nunique_month' , 'total_basket' , 'total_amt' ]
#Create first_year column by extracting from first_date
scv['first_year'] = scv['first_date'].apply(lambda x:x.year)
#Create avg_visit_per_month column
scv['avg_visit_per_month'] = scv['total_basket'] / scv['nunique_month']
#Create avg_spend_per_month column
scv['avg_spend_per_month'] = scv['total_amt'] / scv['nunique_month']
#Create avg_spend_per_time column
scv['avg_spend_per_time'] = scv['total_amt'] / scv['total_basket']
#Transform cust_id from index to column
scv.reset_index(inplace=True)
scv.head()
รูปที่ 6 : ตัวอย่างข้อมูลของ scv

ถัดมาลองมาดูการทำ store_type_ratio โดย Idea คืออยากจะรู้ว่ายอดที่ลูกค้าซื้อทั้งหมดนี่แบ่งเป็นสัดส่วน store_type แต่ละประเภทเป็นจำนวนเท่าใด

#Create pv_store which contains store_type ratio for each cust_id
pv_store = df_tran_prod.pivot_table(index='cust_id',
columns = 'Store_type',
values = 'total_amt',
aggfunc='sum').fillna(0)
#Create total_amt column
pv_store['total_amt'] = pv_store.sum(axis=1)
#Divide each store_type with it's total_amt
pv_store['Flagship store'] = pv_store['Flagship store'] / pv_store['total_amt']
pv_store['MBR'] = pv_store['MBR'] / pv_store['total_amt']
pv_store['TeleShop'] = pv_store['TeleShop'] / pv_store['total_amt']
pv_store['e-Shop'] = pv_store['e-Shop'] / pv_store['total_amt']
#Drop total_amt column
pv_store.drop(columns = 'total_amt' , inplace=True)
#Transform cust_id from index to column
pv_store.reset_index(inplace=True)

pv_store.head()
รูปที่ 7 : ตัวอย่างข้อมูลของ pv_store

ตอนนี้เราจะรู้แล้วว่าสัดส่วนการซื้อของจาก Store_type ของลูกค้าแต่ละคนเป็นอย่างไรเช่น cust_id 266783 นั้นซื้อสินค้าจาก e-Shop เป็นมูลค่า 90% เป็นต้น จากนั้นทำแบบเดียวกันกับ column prod_cat

#Create pv_prod_cat which contains prod_cat ratio for each cust_id
pv_prod_cat = df_tran_prod.pivot_table(index='cust_id' , columns = 'prod_cat', values='total_amt',aggfunc='sum').fillna(0)
#Create total_amt column
pv_prod_cat['total_amt'] = pv_prod_cat.sum(axis=1)
#Divide each store_type with its total_amt (using for loop)
prod_norm_col = ['Bags', 'Books', 'Clothing', 'Electronics', 'Footwear','Home and kitchen']
for col in prod_norm_col:
pv_prod_cat[col] = pv_prod_cat[col] / pv_prod_cat['total_amt']
#Drop total_amt column
pv_prod_cat.drop(columns='total_amt',inplace=True)
#Transform cust_id from index to column
pv_prod_cat.reset_index(inplace= True)
pv_prod_cat.head()
รูปที่ 8 : ตัวอย่างข้อมูลของ pv_prod_cat

จากรูปที่ 8 เราก็จะรู้ลูกค้าแต่ละคนชอบซื้อสินค้าประเภทอะไรบ้าง เช่น cust_id 266783 นั้นซื้อ Books ประมาณ 60% และ ซื้อ Clothing ประมาณ 40% เป็นต้น

จากนั้นเราจะรวมทุก df ที่เกี่ยวกับ customer buying behavior เข้าด้วยกันซึ่งจะมีทั้งหมด 3 df ได้แก่ scv , pv_store และ pv_prod_cat

รูปที่ 9 : ตัวอย่างข้อมูลของ scv, pv_store และ pv_prod_cat

ซึ่งเราสามารถเชื่อมทุกอย่างเข้าด้วยกันจาก column cust_id

#Merge scv, pv_store and pv_prod_cat together
scv_buying = scv.merge(pv_store , how = 'inner' , on = 'cust_id')
scv_buying = scv_buying.merge(pv_prod_cat , how = 'inner' ,on='cust_id')
scv_buying.info()
รูปที่ 10 : เป็นข้อมูลภาพรวมของ scv_buying

Step 4 : Prepare df_cus

คราวนี่เรากลับมาดู df สุดท้ายนั้นก็คือ df_cus

รูปที่ 11 : ตัวอย่างข้อมูล df_cus

เราจะเห็นได้ว่ามี column DOB หรือวันเกิด ซึ่งเราสามารถหาอายุของลูกค้าได้

#Transform dtype of DOB column to datetime
df_cus['DOB'] = pd.to_datetime(df_cus['DOB'])
#Calculate age of customer
df_cus['age'] = round((pd.Timestamp.now() - df_cus['DOB'] ) / np.timedelta64(1, 'Y'))

df_cus.head()
รูปที่ 12 : ตัวอย่างข้อมูล df_cus หลังจากเพิ่ม column age

หมายเหตุ : round ข้อมูลอายุ เนื่องจากอายุเพิ่มขึ้นหรือลดลง 1 ปี ทางธุรกิจอาจจะตัดสินใจไม่ได้แตกต่างกันมากนัก

Step 5 : Combine everything together (merge df_cus with scv_buying)

#Merge df_cus with scv_buying
scv_final = df_cus.merge(scv_buying , left_on = 'customer_Id', right_on = 'cust_id' , how = 'left')
#Drop redundant column (cust_id)
scv_final.drop(columns = 'cust_id' , inplace = True)

scv_final.head()
รูปที่ 13 : ตัวอย่างข้อมูลของ scv_final
รูปที่ 14 : ตารางแสดง informations ของ scv_final

สุดท้ายเราก็ทำการ write ข้อมูลที่อยู่ใน scv_final

#write file name scv_final
scv_final.to_csv('scv_final.csv' , index=False)

เป็นอันเสร็จสิ้นการทำ Single Customer View เบื้องต้น ซึ่งจริงๆแล้วอาจจะสามารถทำได้หลากหลายวิธี หรือมีตัวแปรอื่นที่เราสามารถจะ extract ออกมาจาก Dataset ชุดนี้ อีกมาก สำหรับ Part นี้ก็ขอสิ้นสุดเพียงเท่านี้ แล้วเดี๋ยวเรากลับมาเจอกันใหม่ใน Part หน้านะครับ ขอบคุณคร้าบบบบบ :D

--

--