[실습] 쇼핑몰 고객 주문 데이터 분석

박재한·2022년 3월 18일
1

Machine Learning

목록 보기
4/6

현업에서 데이터가 주어졌을 때, 이 데이터를 가공하고 분석해서 목표 달성을 위한 의사 결정에 도움이 되는 다양한 정보들을 찾아낼 수가 있다.
본 문서는 이러한 정보를 찾기 위해서 처음부터 어떻게 하면 되는지 쇼핑몰 고객 주문 데이터를 예로 들어 설명한다.
쇼핑몰 고객 주문 데이터 분석의 목표는 다음과 같다.

  • 매출 분석
  • 고객 분석
    • 우수 고객 찾기
    • 고객 재구매율 분석
  • push notification 실행 최적 조건 찾기

1. 데이터 확인

먼저 쇼핑몰 고객 주문 데이터를 pandas의 read_csv() 함수등을 통해서 읽어서 retail이라는 DataFrame을 만든다.

1.1 기본적인 내용 확인

DataFrame의 head(), info(), describe()를 사용한다.
retail.head()로 전체 구조를 살펴본다.
Imgur

1.2 column 살펴보기

retail.columns

Index(['InvoiceNo', 'StockCode', 'Description', 'Quantity', 'InvoiceDate',
       'UnitPrice', 'CustomerID', 'Country'],
      dtype='object')

각 column의 의미를 파악한다.

  • invoiceNo: 주문 번호
  • StockCode: 아이템 아이디
  • Description: 상품 설명
  • Quantity: 상품 주문 수량
  • InvoiceDate: 주문 시각
  • UnitPrice: 상품 가격(동일한 통화)
  • CustomerID: 고객 아이디
  • Country: 고객 거주 지역(국가)

1.3 각 column의 정보 살펴보기

retail.info()

<class 'pandas.core.frame.DataFrame'>
RangeIndex: 541909 entries, 0 to 541908
Data columns (total 8 columns):
 #   Column       Non-Null Count   Dtype  
---  ------       --------------   -----  
 0   InvoiceNo    541909 non-null  object 
 1   StockCode    541909 non-null  object 
 2   Description  540455 non-null  object 
 3   Quantity     541909 non-null  int64  
 4   InvoiceDate  541909 non-null  object 
 5   UnitPrice    541909 non-null  float64
 6   CustomerID   406829 non-null  float64
 7   Country      541909 non-null  object 
dtypes: float64(2), int64(1), object(5)
memory usage: 33.1+ MB

전체 541909개의 item이 있는데, 갯수를 잘 보면 Description열과 CustomerID열에 일부가 비어있는 것을 볼 수 있다.
Description은 몰라도 CustomerID는 때에 따라 key값으로도 쓰일 수 있기 때문에 값이 비어 있으면 안된다. CustomerID가 비어있는 데이터는 분석을 할 수 없으므로 해당 row는 제거하도록 한다.

1.4 수치형 데이터의 기본 통계값 확인

retail.describe()
Imgur
Quantity와 UnitPrice의 min값이 음수가 있다. 갯수와 가격에 음수는 말이 안되는 상황이므로 이것도 이후에 찾아서 해당 row를 제거해야 한다.

2. 데이터 정제(cleansing)

데이터 정제(cleansing)는 읽어들인 데이터에서 불필요한 부분을 찾아서 모두 제거하고 분석에 필요한 부분만 남기는 것을 말한다.

  • null data처리
  • business 로직에 맞지 않는 데이터 처리

2.1 null data 처리

retail.isnull().sum()

InvoiceNo           0
StockCode           0
Description      1454
Quantity            0
InvoiceDate         0
UnitPrice           0
CustomerID     135080
Country             0
dtype: int64

CustomerID가 null인 행(row)이 135080개나 있는데 이 행을 모두 제거한다.

retail = retail[pd.notnull(retail['CustomerID'])]
len(retail)
406829

전체 541909개의 행에서 406829개의 행만 남은 것을 알 수 있다.

2.2 Business logic에 맞지 않는 데이터 처리

'Quantity'와 'UnitPrice'열에서 값이 음수인 데이터는 잘못된 것이므로 해당 row도 제거한다.

retail = retail[retail['Quantity'] > 0]
retail = retail[retail['UnitPrice'] > 0]
len(retail)
397884

2.3 데이터 타입 변경

올바른 데이터 타입 적용과 메모리 효율을 위해 데이터 타입을 변경한다.

1) 'CustomerID'의 값을 정수형으로

retail['CustomerID'] = retail['CustomerID'].astype(np.int32)

2) 새로운 column추가

  • Quantity * UnitPrice는 고객의 총 지출 비용(CheckoutPrice)
retail['CheckoutPrice'] = retail['UnitPrice'] * retail['Quantity']
retail.head()

Imgur

2.4 정제된 데이터 저장

retail.to_csv('../Data/OnlineRetailClean.csv')

3. 매출 분석

다양한 기준에 맞춰 매출액 상위권의 정보를 확인한다.

3.1 날짜를 문자열에서 datetime형식으로 변환

날짜를 다양한 기준(연,월,시,분...)에서 비교하려면 문자열 형식으로 된 날짜를 pandas의 datetime형식으로 바꾼다.

retail['InvoiceDate'] = pd.to_datetime(retail['InvoiceDate'], infer_datetime_format = True)
retail.info()
...
5   InvoiceDate    397884 non-null  datetime64[ns]
...

InvoiceDate열의 데이터 형식이 문자열에서 datetime형식으로 바뀐것을 볼 수 있다.

3.2 국가별 매출 분석

전체 매출

total_revenue = retail['CheckoutPrice'].sum()
total_revenue
8911407.904

국가별 매출

rev_by_countries = retail.groupby('Country').sum()['CheckoutPrice'].sort_values()
rev_by_countries
Country
Saudi Arabia            1.459200e+02
Bahrain                 5.484000e+02
Czech Republic          8.267400e+02
RSA                     1.002310e+03
Brazil                  1.143600e+03
European Community      1.300250e+03
Lithuania               1.661060e+03
Lebanon                 1.693880e+03
United Arab Emirates    1.902280e+03
Unspecified             2.667070e+03
Malta                   2.725590e+03
USA                     3.580390e+03
Canada                  3.666380e+03
Iceland                 4.310000e+03
Greece                  4.760520e+03
Israel                  7.221690e+03
Poland                  7.334650e+03
Austria                 1.019868e+04
Cyprus                  1.359038e+04
Italy                   1.748324e+04
Denmark                 1.895534e+04
Channel Islands         2.045044e+04
Singapore               2.127929e+04
Finland                 2.254608e+04
Portugal                3.343989e+04
Norway                  3.616544e+04
Japan                   3.741637e+04
Sweden                  3.837833e+04
Belgium                 4.119634e+04
Switzerland             5.644395e+04
Spain                   6.157711e+04
Australia               1.385213e+05
France                  2.090240e+05
Germany                 2.288671e+05
EIRE                    2.655459e+05
Netherlands             2.854463e+05
United Kingdom          7.308392e+06
Name: CheckoutPrice, dtype: float64

영국(UK)가 가장 매출이 많음을 알 수 있다.

국가별 매출을 그래프로 표기

plot = rev_by_countries.plot(kind='bar', color=COLORS[-1], figsize=(20, 10))
plot.set_xlabel('Country', fontsize=11)
plot.set_ylabel('Revenue', fontsize=11)
plot.set_title('Revenue by Country', fontsize=13)
plot.set_xticklabels(labels=rev_by_countries.index, rotation=45) # x축 눈금을 rev_by_countries의 index(국가명)를 사용한다.

Imgur
국가별 매출을 전체 매출액 대비 비율로 표시

rev_by_countries / total_revenue
Country
Saudi Arabia            0.000016
Bahrain                 0.000062
Czech Republic          0.000093
RSA                     0.000112
Brazil                  0.000128
European Community      0.000146
Lithuania               0.000186
Lebanon                 0.000190
United Arab Emirates    0.000213
Unspecified             0.000299
Malta                   0.000306
USA                     0.000402
Canada                  0.000411
Iceland                 0.000484
Greece                  0.000534
Israel                  0.000810
Poland                  0.000823
Austria                 0.001144
Cyprus                  0.001525
Italy                   0.001962
Denmark                 0.002127
Channel Islands         0.002295
Singapore               0.002388
Finland                 0.002530
Portugal                0.003752
Norway                  0.004058
Japan                   0.004199
Sweden                  0.004307
Belgium                 0.004623
Switzerland             0.006334
Spain                   0.006910
Australia               0.015544
France                  0.023456
Germany                 0.025682
EIRE                    0.029798
Netherlands             0.032032
United Kingdom          0.820116
Name: CheckoutPrice, dtype: float64

영국이 전체 매출의 82%를 차지하고 있어 사실상 영국에만 사업을 집중하고 있음을 알 수 있다.

그래프 출력을 함수로 표현

def plot_bar(df, xlabel, ylabel, title, color=COLORS[0], figsize=(20,10), rotation=45):
    plot = df.plot(kind='bar', color=color, figsize=figsize)
    plot.set_xlabel(xlabel, fontsize=11)
    plot.set_ylabel(ylabel, fontsize=11)
    plot.set_title(title, fontsize=13)
    plot.set_xticklabels(labels=df.index, rotation=rotation)
plot_bar(rev_by_countries, 'Country', 'Revenue', 'Revenue by Country')

앞과 동일한 결과가 나온다.

3.3 월별 매출 분석

def extract_month(date):
    month = str(date.month)
    if date.month < 10:
        month = '0' + month
    return str(date.year) + month
rev_by_month = retail.set_index('InvoiceDate').groupby(extract_month).sum()['CheckoutPrice']
plot_bar(rev_by_month, 'Month', 'Revenue', 'Revenue by Month')

Imgur
매출이 거의 10, 11월에 몰려있다. 12월의 메출은 11월 매출의 반토막이다. 12월 매출이 적은 것은 데이터 수집할 때 마지막 12월의 매출 데이터 전체가 수집이 안되었기 때문이다.

3.4 요일별 매출 분석

월별 매출 분석을 약간 응용하면 된다.

rev_by_dow = retail.set_index('InvoiceDate').groupby(lambda date : date.dayofweek).sum()['CheckoutPrice']
DAY_OF_WEEK = np.array(['Mon', 'Tue', 'Wed', 'Thur', 'Fri', 'Sat', 'Sun']) ## 일반 리스트로는 처리 못하고 numpy의 ndarray로 바꿔 주어야 한다.
rev_by_dow.index = DAY_OF_WEEK[rev_by_dow.index]  ## 요일이 0~6으로 표현되어 이를 문자열로 변경하는 작업
plot_bar(rev_by_dow, 'Week', 'Revenue', 'Revenue by Week')

Imgur
목요일까지는 완만히 매출이 증가하다가 목요일 이후부터 매출이 감소하는 것을 알 수 있다.

3.5 시간별 매출 분석

rev_by_hour = retail.set_index('InvoiceDate').groupby(lambda date : date.hour).sum()['CheckoutPrice']
plot_bar(rev_by_hour, 'Hour', 'Revenue', 'Revenue by Hour')

Imgur
7시를 시작으로 주문이 시작되어 12시까지 증가세, 15시까지 하락을, 15시 이후 부터 급락하는 것을 알 수 있다.

매출 분석으로부터 얻을 수 있는 결론

  • 전체 매출의 82%가 UK에서 발생
  • 11년도의 가장 많은 주문이 발생한 달 11월(12월의 전체 데이터가 반영이 되진 않았음)
  • 11, 12월의 판매량이 압도(블랙프라이데이, 사이버먼데이, 크리스마스 휴일)
  • 일주일중 목요일까지는 성장세를 보이다가, 이후로 하락(토요일에는 주문X)
  • 7시를 시작으로 주문이 시작되어 12시까지 증가세, 15시까지 하락을, 15시 이후 부터 급락)

3.6 매출 상위 제품 분석

Top 3 판매 제품

top_selling = retail.groupby('StockCode').sum()['Quantity'].sort_values(ascending=False)[:3]
top_selling
StockCode
23843    80995
23166    77916
84077    54415
Name: Quantity, dtype: int32

Top 10 매출 제품

top_revenue = retail.groupby('StockCode').sum()['CheckoutPrice'].sort_values(ascending=False)[:10]
top_revenue
StockCode
23843     168469.60
22423     142592.95
85123A    100603.50
85099B     85220.78
23166      81416.73
POST       77803.96
47566      68844.33
84879      56580.34
M          53779.93
23084      51346.20
Name: CheckoutPrice, dtype: float64

3.7 top3 제품의 월별 판매량 추이

monthly_top3 = retail.set_index('InvoiceDate').groupby(['StockCode', extract_month]).sum()[['Quantity', 'CheckoutPrice']].loc[top_selling.index]
plot_bar(monthly_top3['CheckoutPrice'], 'Product/Month', 'Revenue', 'Revenue of Top3 items')

Imgur

4. 고객 분석

4.1 우수 고객 확인

구매 횟수별 우수 고객

retail.groupby('CustomerID').count()['Quantity'].sort_values(ascending=False)

지불 금액별 우수 고객

retail.groupby('CustomerID').sum()['CheckoutPrice'].sort_values(ascending=False)

4.2 고객 재구매(retention) 분석

  • 사용자 cohort(같은 기간동안 같은 경험을 한 고객의 집단), 코호트 분석은 특정 기간 동안 사용자의 참여도 변동 추이를 파악할 수 있다.
  • 월간 사용자 cohort를 바탕으로 월별 재구매율(retention) 분석하기
  • heatmap으로 한눈에 재구매율을 파악 가능

4.1 목표

  • 특정 월에 구매을 한 고객 집단(cohort)이 월(시간)이 지날수록 그대로 유지가 되지 않고 당연히 줄어들 것인데 그 추이를 분석해 본다.
  • 사용자별 구매월 집계
  • 사용자별 구매월에서 최초로 구매한 월 찾기
  • 사용자별 최초로 구매한 월에서 제품 구매한 월의 기간 계산하기
  • 최초로 구매한 월과 제품 구매한 기간으로 그룹을 만들어 해당 그룹의 고객수를 카운팅한다.
  • 지금까지 한 것을 바탕으로 retention 분석을 할 수 있다.

4.2 사용자별 구매월 집계 및 최초로 구매한 월 찾기

먼저 'InvoiceDate' column을 연-월로 단순화 시킨 column('Month' column)을 추가한다.

def get_month_as_datetime(date):
    return datetime(date.year, date.month, 1) # year, month, day

# invoice date field에서 연도와 월만 빼내어 'Month'열에 저장
retail['Month'] = retail['InvoiceDate'].apply(get_month_as_datetime)

'Month' column으로부터 사용자별 최초 구매월을 찾는다.

# 각 사용자당 구매월들 가운데서 최초 구매월을 찾아서 리턴한다.
month_group = retail.groupby('CustomerID')['Month']
retail['MonthStarted'] = month_group.transform(np.min)

Imgur

4.3 사용자별 최초로 구매한 월에서 제품 구매한 월의 기간 계산하기

retail['MonthPassed'] = (retail['Month'].dt.year - retail['MonthStarted'].dt.year) * 12 + \
        (retail['Month'].dt.month - retail['MonthStarted'].dt.month)

Imgur

4.4 기준이 되는 월과 그 월로부터 지난 기간의 고객 수를 계산

def get_unique_no(x):
    return len(np.unique(x))

# 최초 구매월부터 한달씩 지날때마다
cohort_group = retail.groupby(['MonthStarted', 'MonthPassed'])
# 해당 월에 구매한 구매자들의 수를 집계한다.
cohort_df = cohort_group['CustomerID'].apply(get_unique_no).reset_index()

Imgur

4.5 retention 분석

'MonthStarted' 열을 pivot으로 하여 pivot 열에서 중복되는 항목들은 모두 열로 보낸다.

cohort_df = cohort_df.pivot(index='MonthStarted', columns='MonthPassed')

Imgur
백분율로 다시 환산한다.

customer_cohort = cohort_df.div(cohort_df.iloc[:, 0], axis=0) * 100
customer_cohort = customer_cohort.round(decimals=2)

Imgur

  • 위 표를 분석(analysis) 해 보면 2010.12의 경우 최초 구매했던 사람들이 그 다음달(passed 1)에 36.6%로 구매율이 급격히 떨이지는 것을 알 수 있다.
  • 그 다음달(passed 2)에 계속 줄다가 그 다음달(passed 3)에 다시 구매율이 소폭 늘어나는 것을 볼 수 있다.
  • 4개월째에 다시 줄어들었다가 5개월째에 소폭 늘어나고 6, 7개월 줄다가 8,9개월에 다시 늘어나는 것을 볼 수 있다.
  • 이렇게 재구매율이 일정하지 않고 월마다 늘었다가 줄었다가 하는 패턴이 반복되고 있다.
  • 한가지 확실한 것은 최초 구매월 다음 부터는 재구매율이 급격히 떨어지는 것을 볼 수 있다.

heatmap으로 출력

xticks = np.arange(0, 13)
yticks = ['2010/12', '2011/01', '2011/02', '2011/03', '2011/04', '2011/05', '2011/06', '2011/07', '2011/08', '2011/09', '2011/10', '2011/11', '2011/12']

plt.figure(figsize = (15, 8))
sns.heatmap(customer_cohort, 
            annot=True, 
            xticklabels=xticks,
            yticklabels=yticks, 
            fmt='.1f')

Imgur

5. 고객 쿠폰 발송

  • 고객에게 마케팅용 쿠폰을 발송하는 최적 타이밍을 찾는다.
  • 각 시간대에 어느 고객에게 쿠폰을 보내는 것이 가장 좋을지를 찾는다.

5.1 idea

  • 쿠폰 발송 최적의 타이밍은 주문이 가장 많은 시간대일 것이고 이 시간대를 데이터 분석을 통해 찾는다.
  • 시간대별 쿠폰 보낼 고객 선정은, 고객별 가장 주문을 많이 한 시간대를 찾고 그 시간대별로 그룹핑을 하면 각 시간대 별로 가장 주문을 많이 한 고객 리스트가 나온다.
  • 위 2개를 조합하면, 즉 가장 주문 많은 시간대에서 가장 주문을 많이 한 고객들에게 쿠폰을 보내면 가장 효율적으로 쿠폰을 발송 할 수 있을 것이다.

5.2 주문이 가장 많은 시간대 찾기

order_by_hour = retail.set_index('InvoiceDate').groupby(lambda date : date.hour).count()['CustomerID']
plot_bar(order_by_hour, 'hour', '# orders', 'Order by hour')

Imgur
12시(정오)에서 가장 주문이 많을 것을 알 수 있다.

30분 단위로 파악

def half_an_hour(date):
    minute = ':00'
    if date.minute > 30:
        minute = ':30'
    hour = str(date.hour)
    if date.hour < 10 :
        hour = '0' + hour
    
    return hour + minute
    
order_by_hour_half = retail.set_index('InvoiceDate').groupby(half_an_hour).count()['CustomerID']
plot_bar(order_by_hour_half, 'half an hour', '# orders', 'Order by half an hour')

Imgur
12시~12시30분 사이가 가장 주문이 많을 것을 알 수 있다.

5.3 시간대별로 주문을 가장 많이 한 고객 찾기

5.3.1 사용자별로 각 시간대별 주문량 집계하기

order_count_by_hour = retail.set_index('InvoiceDate').groupby(['CustomerID', lambda date : date.hour]).count()['StockCode']
order_count_by_hour
CustomerID    
12346       10     1
12347       8     22
            10    24
            12    47
            13    18
                  ..
18283       15     1
            16    56
            19    87
18287       9      3
            10    67
Name: StockCode, Length: 11205, dtype: int64

각 사용자별로 시간대별 주문량이 계산되어 나온다.
12347 사용자의 경우만 살펴보면 위 출력에서는 짤렸지만 14시에 주문량이 60으로 가장 많을 것을 알 수 있다.

5.3.2 사용자별로 최대 주문이 있는 시간대 집계하기

idx = order_count_by_hour.groupby('CustomerID').idxmax()
idx
CustomerID
12346    (12346, 10)
12347    (12347, 14)
12348    (12348, 19)
12349     (12349, 9)
12350    (12350, 16)
            ...     
18280     (18280, 9)
18281    (18281, 10)
18282    (18282, 13)
18283    (18283, 14)
18287    (18287, 10)
Name: StockCode, Length: 4338, dtype: object

사용자별 시간대별 주문량에서 사용자별로 그룹을 만들면서 최대 주문이 있는 시간으로 중복 행(row)들을 연산한다. DataFrame의 idxmax는 가장 큰 값이 있는 행의 index를 리턴한다. 여기서의 index는 사용자와 시간의 조합이 된다.
예를 들어 12347 사용자(CustomerID)의 경우는 14시가 가장 주문이 많은 시간대이다.
5.3.2의 index를 5.3.1에 적용한다.

result = order_count_by_hour.loc[idx]
result.reset_index()

Imgur

5.3.3 시간대별 가장 주문을 많이 한 고객의 리스트 찾기

앞의 5.3.2의 결과에 시간대별로 그룹핑을 한다.
그러면 시간대별로 가장 주문이 많은 사용자들이 집계가 되는데 여기에는 'CustomerID', 'StockCode'가 있는 행의 index가 집계가 된다.
여기서 다시 'CustomerID'만 뽑아야 한다.

result.reset_index().groupby('level_1').groups

결과는 시간대별로 그룹원들의 index list가 반환된다.
예를 들어 10시의 groups 값을 보면,

idxs = result.reset_index().groupby('level_1').groups[10]
idxs
Int64Index([   0,   11,   21,   27,   28,   41,   42,   45,   49,   51,
            ...
            4290, 4292, 4294, 4296, 4297, 4319, 4322, 4330, 4334, 4337],
           dtype='int64', length=510)

10시에 510개의 건이 있는데 각각의 행의 index번호가 0, 11, 21, ..., 4334, 4337이다.(이 행에 'CustomerID', 'StockCode'가 있다.)
여기에 'CustomerID'만 따로 뽑으려면,

result.reset_index().loc[idxs]['CustomerID'].reset_index().drop(['index'], axis=1)

Imgur
이렇게 하면 오전 10시에 가장 많은 주문을 한 고객의 리스트가 나오게 된다.

profile
바쁘게 부지런하게 논리적으로 살자!!!

1개의 댓글

comment-user-thumbnail
2024년 7월 28일

해당 데이터셋을 따라서 직접 코딩해 보고 있는데요, 국가별 매출 분석에서
rev_by_countries = retail.groupby('Country').sum()['CheckoutPrice'].sort_values()
rev_by_countries

이 코드를 실행할 때 계속 *표시가 뜨고 Interrupt kernel도 안 먹혀요. 코드를 수정하면 해결될 거 같은데 수정해 주실 수 있나요?

답글 달기