마케팅/실습

파이썬으로 대형 마트 고객 데이터 분석하기

퍼포마첼라 2025. 3. 23. 17:28

 

  • 1.데이터 탐색 및 전처리
    • 라이브러리 불러오기
    • 데이터 불러오기
    • 데이터 탐색하기
    • annual_income 컬럼 결측값 처리하기
    • 출생 연도를 나이로 바꾸기
    • 구매 금액 합계 및 구매 횟수 합계 계산하기
    • 불필요한 컬럼 제거하기
  • 2.인구통계학적 고객 정보 분석
    • 나이 분포 파악하기
  • 3.RFM 고객 세그먼트 분류
    • Recency, Frequency, Monetary 등급 매기기
    • 가중합을 이용해 RFM 고객 세그먼트 분류하기
  • 4.세그먼트별 특성 및 소비 성향 분석
    • 세그먼트마다 연령대 분포 파악하기
    • 세그먼트마다 가족 구성 분포 파악하기
    • 세그먼트마다 품목별 매출 기여도 파악하기
    • 세그먼트마다 프로모션 참여율 파악하기

 


1. 데이터 탐색 및 전처리

 

라이브러리 불러오기

일단 데이터 분석을 위한 라이브러리들을 불러옵시다. 해설 노트에서는 pandas, Matplotlib Pyplot, seaborn 사용할 건데요. 추가로 필요한 라이브러리가 있다면 자유롭게 불러와서 사용하시기 바랍니다.

 

import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns

 

 

데이터 불러오기

그럼 분석할 데이터를 불러올 차례입니다. pandas read_csv() 함수를 이용해 csv 파일을 DataFrame으로 불러오면 되는데요. csv 파일에서 csv 뜻이 'comma-separated values' 였다는 기억나시나요? 뜻처럼 csv 파일에 적힌 값들은 일반적으로 쉼표로 구분됩니다. 하지만 이번에 저희가 다룰 csv 파일에는 값들이 쉼표가 아니라 탭으로 구분되어 있어요. 그래서 read_csv() 함수를 사용할 csv 파일 경로를 전달하면서 sep 파라미터에 '\t'라는 값도 전달해야 데이터를 제대로 불러올 있습니다. 이렇게 불러온 DataFrame customer_df라는 이름의 변수에 저장해 두겠습니다.

customer_df = pd.read_csv('data/customer_data.csv', sep='\t')

 

 

데이터 탐색하기

본격적인 데이터 분석에 앞서 데이터를 탐색해 볼게요. 우선 shape 속성을 통해 데이터의 크기를 알아봅시다. (2240, 23)라고 나오는 걸 보니 2,240개의 로우와 23개의 컬럼으로 이루어진 데이터임을 알 수 있죠.

 

customer_df.shape

#(2240, 23)

 

그다음으로 head() 함수를 이용해 데이터가 어떻게 생겼는지 대략적으로 살펴보겠습니다. 따로 파라미터를 설정하지 않으면 데이터의 첫 다섯 줄을 확인할 수 있죠. 컬럼 개수가 많아 가로 스크롤바가 생길 텐데 Shift + 마우스휠을 통해 가로 방향으로 움직이며 데이터를 볼 수 있습니다.

customer_df.head()

 

컬럼이 다 표시되지 않고 중간에 몇 가지가 생략된 채 ...로 화면에 나온다면 set_option() 함수를 이용해 display.max_columns 옵션을 None으로 설정해 주세요. 화면에 표시되는 컬럼 개수의 제한을 해제할 수 있습니다. 다시 head() 함수를 호출하면 컬럼이 빠짐없이 표시될 거예요.

 

pd.set_option('display.max_columns', None)
customer_df.head()

 

이번에는 info() 함수를 이용해 각 컬럼의 데이터 타입과 결측값이 아닌 non-null 데이터의 개수를 확인해 봅시다. 아래 출력 결과를 보면 날짜 데이터인 signup_ym과 혼인 상태를 나타내는 범주형 데이터인 marital_status가 object 타입으로 지정되어 있습니다. 여기에 annual_income이 float64 타입인 걸 제외하면 나머지는 모두 int64 타입이네요. 그리고 데이터 총개수가 2,240개인데 annual_income의 non-null 데이터만 2,216개인 걸 보니 annual_income 컬럼에 결측값이 있는 모양이군요.

 

customer_df.info()

 

다음으로 describe() 함수를 이용해 통계 정보를 확인해 볼게요. 이 함수는 기본적으로 숫자 데이터의 통계 정보를 보여주는데요. 만약 다른 데이터 타입을 가진 컬럼들도 확인하고 싶다면 include라는 파라미터에 'all'을 넘겨주면 됩니다.

 

customer_df.describe(include='all')

 

데이터 타입이 object인 컬럼의 정보를 조금 더 알아봅시다. 각 컬럼마다 어떤 값들이 몇 개씩 존재하는지 파악해 볼게요. customer_df의 컬럼 중 데이터 타입이 object인 컬럼들을 for문으로 순회하면서 컬럼의 이름, 그리고 unique한 값과 그 개수까지 출력하겠습니다. 먼저 customer_dfcolumns에서 불린 인덱싱을 통해 데이터 타입이 object인 컬럼들을 뽑아 columns_object_dtype이라는 변수에 담아 줄게요.

 

columns_object_dtype = customer_df.columns[customer_df.dtypes == 'object']
columns_object_dtype

#Index(['signup_ym', 'marital_status'], dtype='object')

 

columns_object_dtype 변수에 대해 for문을 사용하면 데이터 타입이 object인 컬럼을 순회할 수 있습니다. 컬럼마다  unique() 함수를 통해 unique한 값을 구하고, 더 알아보기 쉽게 출력할 수 있도록 sorted() 함수로 정렬까지 해 줄게요. 출력 결과를 보니 signup_ym에는 2020년 10월부터 2022년 9월까지의 연월 값이 담겨 있고, marital_status에는 혼인 상태와 관련된 항목 네 가지가 존재하네요.

 

for col in columns_object_dtype:
    unique_values = sorted(customer_df[col].unique())
    print(f'{col}: {len(unique_values)}개')
    print(unique_values, '\n')

 

 

annual_income 컬럼 결측값 처리하기

 

앞서 데이터를 탐색하면서 info() 함수 출력 결과를 통해 annual_income 컬럼에 결측값이 있다는 사실을 파악할 수 있었는데요. isna()sum() 함수를 이용해 결측값이 있는지 다시 한번 확인해 보겠습니다. 결과를 보니 역시 annual_income 컬럼에 결측값이 있네요.

 

customer_df.isna().sum()

 

어떻게든 다른 값으로 채워 넣을 수도 있겠지만 24개 데이터는 전체 데이터의 1% 수준이니 제거해도 데이터 분석에 큰 영향은 없을 듯합니다. 간단하게 dropna() 함수를 써서 결측값이 포함된 고객 데이터 24개를 없애 줄게요. 그다음 결측값 제거가 잘 되었는지 확인하기 위해 다시 isna()sum() 함수를 함께 호출해 보겠습니다. 결측값이 없어진 게 잘 보이네요.

 

customer_df = customer_df.dropna()
customer_df.isna().sum()

 

 

출생 연도를 나이로 바꾸기

 

지금까지 데이터를 탐색하고 결측값까지 처리해 봤는데요. 이번에는 간단히 데이터를 가공해 볼게요. 먼저 고객의 출생 연도를 조금 더 직관적인 값인 나이로 바꾸겠습니다. 출생 연도에서 나이를 계산하려면 기준 시점이 있어야겠죠? 실습 설명에서 언급했듯이 2023년 1월이 기준 시점이므로 간단히 2023에서 birth_year 컬럼을 빼 나이를 구해 봅시다. 그리고 이 나이 데이터를 기존의 birth_year 컬럼에 넣어 줄게요.

 

customer_df['birth_year'] = 2023 - customer_df['birth_year']

 

이렇게 되면 birth_year 컬럼에 나이 데이터가 들어 있는 셈이니 컬럼 이름을 age로 바꿀게요. 컬럼 이름을 변경할 때에는 rename() 함수를 이용하면 됩니다. columns 파라미터에 기존 컬럼 이름과 새 컬럼 이름을 짝 지은 딕셔너리를 전달해 주세요. rename() 함수의 결과는 customer_df에 다시 넣어 줍시다. 데이터를 출력해 보면 컬럼 이름이 age로 잘 바뀌었고 나이 데이터가 들어 있는 걸 확인할 수 있어요.

 

customer_df = customer_df.rename(columns={'birth_year': 'age'})
customer_df.head()

 

 

구매 금액 합계 및 구매 횟수 합계 계산하기

 

원본 데이터에는 품목별 구매 금액, 구매 창구에 따른 구매 횟수 및 할인 구매 횟수가 기록되어 있습니다. 보다 통합적으로 고객을 분석하기 위해 구매 금액 합계와 구매 횟수 합계를 계산하여 새로운 컬럼으로 만들어 볼게요.

먼저 구매 금액 합계부터 구해 봅시다. amount_alcohol, amount_fruit, amount_meat, amount_fish, amount_snack, amount_general 컬럼은 각각 품목별 구매 금액을 나타내고 있습니다. 여섯 컬럼을 모두 더해 data_amount_total이라는 변수에 넣어 줄게요.

 

data_amount_total = (
    customer_df['amount_alcohol']
    + customer_df['amount_fruit']
    + customer_df['amount_meat']
    + customer_df['amount_fish']
    + customer_df['amount_snack']
    + customer_df['amount_general']
)

 

이렇게 합한 데이터를 새로운 컬럼으로 customer_df에 추가하겠습니다. 컬럼 이름은 amount_total이라고 해 볼게요. customer_df['amount_total'] = data_amount_total처럼 작성하면 간단하게 컬럼이 추가될 텐데요. 이럴 경우 새로운 컬럼이 데이터 오른쪽 끝에 위치하게 됩니다. 데이터에 컬럼이 워낙 많아서 오른쪽 끝보다는 품목별 구매 금액과 관련된 컬럼들 가까이에 위치시키면 더 좋을 듯해요. amount_로 시작하는 컬럼 중 amount_general이 가장 오른쪽에 있으니, amount_total 컬럼은  amount_general 컬럼 오른쪽에 두면 될 것 같네요.

 

 

원하는 위치에 컬럼을 삽입하고 싶을 때에는 insert() 함수를 사용하면 됩니다. loc 파라미터에는 컬럼을 삽입할 자리의 인덱스를, column 파라미터에는 새 컬럼 이름을, value 파라미터에는 새 컬럼 데이터를 전달해야 하죠. 새 컬럼 이름과 데이터는 이미 준비됐으니 컬럼을 삽입할 위치의 인덱스를 구해야겠군요.

컬럼 인덱스는 첫 컬럼이 0이고 오른쪽으로 갈수록 1씩 커집니다. 따라서 amount_general 컬럼의 오른쪽을 나타내는 인덱스는 amount_general 컬럼의 인덱스에 1을 더한 값이 될 거예요. 그러면 먼저 amount_general 컬럼의 인덱스를 알아야겠죠? DataFrame columns의 get_loc() 함수에 컬럼 이름을 입력해 주면 그 컬럼의 인덱스를 구할 수 있습니다. amount_general 컬럼의 인덱스를 구해 index_amount_general이라는 변수에 넣어 줄게요.

 

index_amount_general = customer_df.columns.get_loc('amount_general')

 

이제 insert() 함수를 호출할 차례입니다. loc 파라미터 값으로 index_amount_general에 1을 더해 넣어 줄게요. 이어서 column 파라미터에는 'amount_total'을, value 파라미터에는 data_amount_total를 전달하면 되겠죠.

 

customer_df.insert(
    loc=index_amount_general + 1,
    column='amount_total',
    value=data_amount_total,
)
customer_df.head()

 

같은 방식으로 총 구매 횟수를 계산해 num_purchase_total이라는 컬럼을 만들어 볼게요. 원본 데이터에서 구매 횟수와 관련된 컬럼은 num_purchase_web, num_purchase_store, num_purchase_discount인데요. 세 컬럼 모두 중복되는 구매 내역 없이 독립적이라고 가정하면, 간단히 세 컬럼을 더해 num_purchase_total 컬럼을 만들 수 있습니다. 이번에는 num_purchase_total 컬럼을 num_purchase_discount 컬럼의 오른쪽에 두면 좋을 것 같네요.

 

num_purchase_total = (
    customer_df['num_purchase_web']
    + customer_df['num_purchase_store']
    + customer_df['num_purchase_discount']
)

index_num_purchase_discount = customer_df.columns.get_loc('num_purchase_discount')
customer_df.insert(
    loc=index_num_purchase_discount + 1,
    column='num_purchase_total',
    value=num_purchase_total,
)
customer_df.head()

 

 

불필요한 컬럼 제거하기

 

구매 금액 합계와 구매 횟수 합계 컬럼을 추가하면서 컬럼의 개수가 25개가 됐네요. 컬럼의 개수가 많으면 많을수록 고객을 분석할 재료가 많아지지만, 그만큼 분석이 더 어려워지기도 하고 분석에 별로 도움이 되지 않는 컬럼이 존재할 가능성도 커집니다. 불필요한 컬럼을 미리 제거한다면 데이터 분석을 더 효율적으로 할 수 있을 거예요. 현재 customer_df에 불필요한 컬럼이 있는지 한번 생각해 봅시다.

먼저 ID 컬럼을 볼게요. 고객 ID는 고객마다 임의로 부여된 값이므로 고객 특성을 분석하는 것과는 관련이 없을 겁니다. 이를테면 ID 값이 클수록 구매 금액이 크다거나, ID 값이 작을수록 구매 횟수가 적다거나 하는 경향이 없다는 뜻이죠. 그러니 삭제해도 문제가 되지 않을 거예요.

다음으로는 revenue 컬럼입니다. 수익을 뜻한다고 하니 annual_income과 비슷한 컬럼일 것 같은데요. describe() 함수를 통해 이 컬럼의 통계 수치를 확인해 볼게요. 평균이 11이고 표준편차가 0이라고 나오는군요. 다시 말해 revenue 컬럼에는 11이라는 값만 존재한다, 즉 모든 고객에게 똑같은 값이 주어져 있다는 뜻입니다. 결국 고객을 분석하는 데 필요가 없을 테니 이 컬럼 역시 지워도 되겠죠. 데이터만 봐서는 이런 컬럼이 존재하는 이유를 알 수 없는데요. 어쩌면 데이터 수집 과정에 문제가 있었을지도 모르겠습니다. 데이터를 다루다 보면 이렇게 분석에 도움이 되지 않거나 의미가 불분명한 컬럼이 존재하는 경우도 많아요. 그런 컬럼이 있는지 잘 판단하여 대응할 수 있어야 합니다.

 

customer_df['revenue'].describe()

 

그럼 ID 컬럼과 revenue 컬럼을 없애기 위해 drop() 함수를 이용해 볼게요. columns 파라미터에 지우고자 하는 컬럼의 이름들을 리스트로 전달해 주면 됩니다.

 

customer_df = customer_df.drop(columns=['ID', 'revenue'])
customer_df.head()

 


 

2. 인구통계학적 고객 정보 분석

 

나이 분포 파악하기

데이터 탐색과 전처리를 마쳤으니 본격적으로 데이터를 분석해 볼 차례입니다. 데이터 분석 과정에서는 그래프를 그려 데이터를 시각화하는 경우도 많은데요. 그래프에 한글 텍스트를 제대로 표시하려면 한글 지원 폰트를 설정해 줘야 합니다. 코드잇 실습 환경에는 네이버에서 제공하는 나눔고딕 폰트가 설치되어 있으니 실행기에서 아래 코드를 통해 폰트를 설정해 주세요.

 

plt.rc('font', family='NanumGothic')

 

PC에 직접 개발 환경을 구축해 실습하고 있다면 운영체제에 맞춰 폰트를 사용하면 됩니다. Windows는 맑은 고딕, macOS는 애플 고딕으로 설정해 주세요.

 

# Windows
plt.rc('font', family='Malgun Gothic')

# macOS
plt.rc('font', family='AppleGothic')

 

폰트 설정을 마쳤다면 이제 고객 나이가 어떻게 분포되어 있는지 살펴보겠습니다. seaborn의 histplot()을 이용해 히스토그램을 그려 볼게요. 보기 좋게 그래프 크기와 제목, 그리고 X축과 Y축의 이름까지 설정하겠습니다.

 

plt.rcParams['figure.figsize'] = (10, 5)
sns.histplot(data=customer_df['age'])
plt.title('고객 나이 분포')
plt.xlabel('나이(세)')
plt.ylabel('고객 수(명)')

 

30대 후반에서 40대 초반 고객의 수가 많은 것으로 보이네요. 반면 20대와 70대에 가까워질수록 점점 고객의 수가 줄어들고 있습니다. 그런데 자세히 보니 나이가 120세에 가까운 고객이 몇 명 있다고 나오는군요. 생물학적으로 불가능에 가까운 데이터죠.

고객 데이터를 나이가 많은 순으로 정렬해 실제로 어떤 데이터가 있는지 확인해 봅시다. sort_values() 함수를 사용하면 되는데요. age 컬럼을 기준으로 정렬하므로 by 파라미터에는 'age'를 전달하고, 내림차순으로 정렬해야 하니 ascending 파라미터에는 False를 넣어 줍시다.

 

customer_df.sort_values(by='age', ascending=False)

 

고객 중 가장 어린 사람은 19세로 납득할 만한 반면, 가장 나이가 많은 사람은 122세로 현실적이지 않습니다. 그 밑으로 나이가 115세, 116세인 고객도 존재하네요. 아마 생년이 잘못 기입되어 발생한 이상값인 것 같아요. 나이를 제외한 나머지 컬럼들을 이용해 유사한 고객을 찾아서 나이를 추정해 볼 수도 있겠지만, 이상값이 포함된 고객이 세 명뿐이니 그냥 제거하겠습니다. 115세 다음으로 나이가 많은 고객이 75세이므로 대강 100세 미만의 고객만 골라 customer_df 변수에 다시 담아 줄게요.

 

customer_df = customer_df[customer_df['age'] < 100]

 

히스토그램을 새로 그려 보면 전보다 더 현실적인 나이 분포를 확인할 수 있습니다.

 

sns.histplot(data=customer_df['age'])
plt.title('고객 나이 분포')
plt.xlabel('나이(세)')
plt.ylabel('고객 수(명)')

 

지금의 age 컬럼은 고객의 나이가 담긴 정수형 데이터인데요. 19세부터 75세까지 워낙 다양하게 분포되어 있어 개별 나이를 기준으로 고객을 분석하기는 어렵습니다. 나이를 10년 단위로 묶어 연령대 데이터로 바꿔 주면 각 연령대마다 경향이나 특성을 파악하고 분석하기 쉬워질 거예요. 이렇게 연속적인 데이터를 특정 구간으로 나누어 범주형 데이터로 바꾸는 과정을 데이터 구간화(Binning)라고 합니다. 데이터를 구간화할 때 자주 사용되는 함수로는 cut()이 있어요. cut() 함수를 이용하면 사용자가 지정한 구간대로 데이터를 나눌 수 있죠.

현재 데이터에는 고객 나이가 19세부터 75세까지 분포되어 있으니 연령대는 10대부터 70대까지 존재할 겁니다. 이때 10대는 10세 이상 20세 미만, 20대는 20세 이상 30세 미만, 그리고 70대는 70세 이상 80세 미만을 뜻하고요. 이렇게 나눈 구간을 리스트로 표현하여 age_bins라는 변수에 먼저 넣어 주겠습니다.

 

age_bins = list(range(10, 81, 10))
age_bins

#[10, 20, 30, 40, 50, 60, 70, 80]

 

다음으로 각 구간의 이름이 담긴 문자열 리스트를 만들어 볼게요. 연령대가 10대부터 70대까지 존재하므로 아까 만들었던 age_bins에서 마지막 원소인 80을 제외하고 각 원소 값의 뒤에 ‘대’ 자를 붙이면 될 거예요. 파이썬은 어떤 리스트를 바탕으로 새로운 리스트를 만드는 데 list comprehension이라는 간편한 방법을 제공하는데요. [] 안에 우선 새로운 리스트 원소를 만드는 코드를 쓰고, 그 뒤에는 바탕이 되는 리스트를 순회하도록 for문을 써 주면 됩니다. 참고로 age_bins에서 마지막 원소를 제외하려면 age_bins[:-1]처럼 사용하면 되겠죠. 이를 종합하여 list comprehension 코드를 작성해 문자열 리스트 변수 age_labels를 만들겠습니다.

 

age_labels = [f'{x}대' for x in age_bins[:-1]]
age_labels

#['10대', '20대', '30대', '40대', '50대', '60대', '70대']

 

cut() 함수를 호출할 때에는 x 파라미터에 구간화할 데이터를, bins 파라미터에 구간을 나타내는 리스트를, labels 파라미터에 구간 이름을 나타내는 리스트를 전달합니다. 마지막으로 right 파라미터가 있는데요. 구간의 오른쪽 값을 포함할지 안 할지를 의미합니다. 연령대 구간을 나눌 때 왼쪽 값 '이상', 오른쪽 값 '미만'으로 경계를 정했으니 오른쪽 값이 포함되면 안 되겠죠. 즉 False를 전달해 주면 됩니다.

 

age_group = pd.cut(x=customer_df['age'], bins=age_bins, labels=age_labels, right=False)

 

이제 cut() 함수로 구간화한 데이터를 customer_df에 추가하면 됩니다. insert() 함수를 써서 age 컬럼 오른쪽에 age_group 이라는 이름으로 컬럼을 넣어 줄게요.

 

customer_df.insert(
    loc=customer_df.columns.get_loc('age') + 1,
    column='age_group',
    value=age_group,
)
customer_df.head()

 

value_counts() 함수로 연령대별 고객 수를 확인해 보겠습니다.

 

customer_df['age_group'].value_counts()

 

아무래도 10대와 70대의 숫자가 너무 적은 것 같네요. 이렇게 특정 연령대에서 표본의 수가 너무 적으면 해당 연령대에 대한 통계 수치의 신뢰성이 떨어지고 분석 결과 역시 왜곡되기 쉽습니다. 그래서 10대와 20대를 20대 이하로, 60대와 70대를 60대 이상으로 바꾸어 연령대 값을 새롭게 구해 줄게요.

이렇게 데이터 내 특정 값을 다른 값으로 바꾸고 싶을 때에는 replace() 함수를 이용하면 됩니다. 이때 replace() 함수에 값을 어떻게 바꿀지 입력해야 하는데요. 지금처럼 바꾸고자 하는 값이 여럿일 경우에는 변경 사항을 딕셔너리로 나타내면 좋아요. 딕셔너리의 키에는 기존 값을, 키에 대응하는 밸류에는 새로운 값을 넣어 알맞게 짝 지어 주세요. 그다음 age_group 컬럼 데이터에 대해 replace() 함수를 호출하고 그 결과를 age_group 컬럼으로 다시 넣어 줍시다.

 

age_group_replace_dict = {
    '10대': '20대 이하',
    '20대': '20대 이하',
    '60대': '60대 이상',
    '70대': '60대 이상',
}
customer_df['age_group'] = customer_df['age_group'].replace(age_group_replace_dict)

 

이제 value_counts() 함수를 호출하면 의도한 대로 연령대 데이터가 바뀐 걸 확인할 수 있습니다.

 

customer_df['age_group'].value_counts()

 

 

연 소득 분포 파악하기

 

이번에는 고객 연 소득 분포를 살펴볼게요. 나이 분포 때와 마찬가지로 seaborn의 histplot()을 이용하면 한눈에 확인할 수 있겠죠. 이번에도 그래프 크기와 제목, 그리고 X축과 Y축의 이름까지 설정하겠습니다.

sns.histplot(data=customer_df['annual_income'])
plt.title('고객 연 소득 분포')
plt.xlabel('연 소득(원)')
plt.ylabel('고객 수(명)')

 

그래프를 보니 연 소득에도 이상값이 존재하는 것 같은데요. 박스 플롯을 그려서 다시 확인해 보겠습니다. 간단히 이상값 존재를 확인하기 위해서이니 별다른 설정은 하지 않을게요.

 

sns.boxplot(data=customer_df, x='annual_income')

 

역시 연 소득이 높은 쪽에 이상값으로 분류된 데이터가 몇 개 존재하네요. 나이가 110세를 넘는 고객이 존재하는 건 생물학적으로 불가능에 가깝지만, 연 소득이 다른 사람들에 비해 높은 고객은 충분히 존재할 수 있겠죠. 결국 연 소득 이상값이 오류 때문에 생긴 건지 실제 값인지 이 데이터만으로는 알 수 없습니다. 다만 소득이 예외적으로 높은 고객이 있을 시 소득과 소비 사이의 관계를 분석할 때 왜곡이 발생할 수 있고, 연 소득이 이상 범위에 있는 고객의 수가 많지 않으니 그냥 제거해 줄게요.

이상값 제거에는 IQR 방식을 사용하겠습니다. quantile() 함수로 Q1 값과 Q3 값을 구해 그 둘의 차이인 IQR까지 구한 뒤, Q1 - 1.5 * IQR과 Q3 + 1.5 * IQR 사이에 있는 데이터만 남기는 방법입니다.

 

income = customer_df['annual_income']
q1 = income.quantile(0.25)
q3 = income.quantile(0.75)
iqr = q3 - q1
lower_bound = q1 - 1.5 * iqr
upper_bound = q3 + 1.5 * iqr

normal_condition = (lower_bound <= income) & (income <= upper_bound)
customer_df = customer_df[normal_condition]

 

다시 박스 플롯을 그려 보면 이상값이 제거된 걸 확인할 수 있습니다.

 

sns.boxplot(data=customer_df, x='annual_income')

 


 

3. RFM 고객 세그먼트 분류

 

Recency, Frequency, Monetary 등급 매기기

 

우선 Recency, Frequency, Monetary 각 항목의 등급 개수를 지정해야 합니다. 편의상 3개로 하여 1등급, 2등급, 3등급으로 나누겠습니다. 이때 등급의 숫자는 클수록 더 긍정적인 것으로 가정할게요. 즉, Recency 등급의 숫자는 클수록 더 최근에 구매했다는 뜻이고, Frequency 등급의 숫자는 클수록 더 자주 구매했다는 뜻이며, Monetary 등급의 숫자는 클수록 돈을 더 많이 썼다는 의미가 됩니다. 등급 개수는 앞으로 자주 사용할 예정이라서 미리 num_grades라는 변수에 담아 놓겠습니다.

 

num_grades = 3

 

그리고 customer_df에 Recency, Frequency, Monetary 각 항목의 등급을 숫자로 표시하기 위해 grade_labels라는 리스트 변수도 미리 만들어 놓을게요.

 

grade_labels = list(range(1, num_grades + 1))
grade_labels

#[1, 2, 3]

 

먼저 Recency 등급부터 매겨 볼게요. customer_df에서는 recency 컬럼을 보면 되겠군요. 이번 해설 노트에서는 편의상 각 등급마다 고객의 수를 최대한 균등하게 나누겠습니다. 이런 방식에 사용되는 함수는 qcut()으로, x 파라미터에 넣어 준 데이터를 q 파라미터에 넣어 준 값만큼 등분해 줍니다. 이 함수에서는 labels 파라미터를 통해 각 등급을 어떻게 표시할지 지정할 수 있는데요. recency 컬럼 값이 작은 고객일수록 최근에 구매를 했다는 뜻이니 Recency 등급을 더 높게 부여해야 합니다. 즉, qcut()을 통해 고객을 세 그룹으로 나누었을 때 앞 그룹이 3등급, 중간 그룹이 2등급, 뒷 그룹이 1등급이 되는 거죠. 따라서 labels 파라미터에 grade_labels를 역순으로 넣어 줘야 해요. 역순으로 뒤집힌 리스트는 원본 리스트를 [::-1]처럼 슬라이싱 해 주면 얻을 수 있습니다. 그럼 qcut() 함수의 xrecency 컬럼 데이터를 전달하고, q에는 num_grades 변수를 전달하고, labels에는 grade_labels[::-1]을 전달해 볼게요.

 

recency_grade = pd.qcut(x=customer_df['recency'], q=num_grades, labels=grade_labels[::-1])

 

이렇게 만들어진 Recency 등급 데이터는 recency_grade라는 이름의 컬럼으로 customer_df에 추가하겠습니다.

 

customer_df['recency_grade'] = recency_grade

 

recency_grade 컬럼에 value_counts()를 적용해 보면, 1등급부터 3등급까지 정확히 똑같이 분배된 건 아니지만 비슷한 비율로 나뉘었음을 확인할 수 있습니다.

 

customer_df['recency_grade'].value_counts()

 

그럼 Recency 등급에 따라 매출 기여도가 어떻게 다른지 시각화해 볼까요? 각 등급의 매출 기여도는 전체 매출 대비 해당 등급의 매출 비율로 구할 수 있습니다. 그렇다면 먼저 recency_grade 컬럼 기준으로 groupby 한 다음 sum() 함수를 써서 등급별 매출 합계를 구해야겠군요. sum() 함수를 사용할 때에는 숫자 값들만 합을 구할 수 있게 numeric_only 값을 True로 설정하겠습니다. 마지막으로 reset_index() 함수까지 사용해 주면 깔끔하게 recency_grade도 컬럼으로 만들 수 있어요. 이 groupby 결과는 groupby_recency_grade 변수에 담겠습니다. 1등급에서 3등급으로 갈수록 recency 컬럼의 값이 줄어드는 걸 보면 등급이 제대로 매겨진 것 같군요.

 

groupby_recency_grade = customer_df.groupby('recency_grade').sum(numeric_only=True).reset_index()
groupby_recency_grade

 

groupby_recency_gradeamount_total 컬럼에 대해 파이 차트를 그려 주면 등급별로 매출 기여도가 어떻게 다른지 한눈에 볼 수 있습니다. plot() 함수에서 kind'pie'로 설정해 주면 되겠죠.

 

groupby_recency_grade['amount_total'].plot(kind='pie')

 

이렇게만 그려서는 정보를 파악하기가 쉽지 않군요. 그래프를 조금 더 알아보기 좋게 파라미터들을 추가로 설정해 봅시다. autopct 파라미터에 '%.1f%%'를 넣어 주면 파이 차트 내 각 영역의 비중 퍼센트를 소수 첫 번째 자리까지 표시할 수 있습니다. labels 파라미터로는 각 영역에 이름을 붙일 수 있어서, 등급을 나타내는 숫자 뒤에 '등급'을 붙인 문자열 리스트를 list comprehension으로 만들어 넣어 줄게요. 앞서 qcut() 함수를 통해 구간화할 때 grade_labels를 역순으로 뒤집어서 전달했으니, 이번에도 영역 이름을 3등급부터 1등급 순으로 넣어 줘야 합니다. title에는 그래프 제목을 적당히 전달해 줄게요. ylabel은 따로 지정하지 않으면 컬럼 이름이 좌측에 표시되는데 이를 막기 위해 빈 문자열을 넣어 주겠습니다. 이렇게 다시 그려 보면 전보다 그래프를 파악하기가 더 쉬워지죠. 결과를 보니 Recency 등급에 따라서는 매출 기여도의 차이가 거의 없군요.

 

groupby_recency_grade['amount_total'].plot(
    kind='pie',
    autopct='%.1f%%',
    labels=[f'{x}등급' for x in grade_labels[::-1]],
    title='Recency 등급별 매출 기여도',
    ylabel='',
)

 

이번에는 Frequency 등급을 매겨 볼게요. Frequency와 관련된 컬럼은 num_purchase_total이죠. Recency와 마찬가지로 qcut() 함수를 이용해 등급을 나누겠습니다. 주의할 점이 하나 있는데요. num_purchase_total 컬럼은 값이 클수록 더 자주 구매를 했다는 뜻이므로 더 높은 등급이 매겨져야 합니다. 그래서 이번에는 labelsgrade_labels 리스트를 그대로 전달해 줘야 해요. qcut() 함수 결과는 customer_dffrequency_grade라는 이름의 컬럼으로 추가하겠습니다.

 

customer_df['frequency_grade'] = pd.qcut(
    x=customer_df['num_purchase_total'], q=num_grades, labels=grade_labels
)

 

frequency_grade 컬럼에 value_counts() 함수를 적용한 결과를 보면 1등급 고객의 수가 다른 등급보다 더 많은데요. num_purchase_total 컬럼 데이터에 unique 한 값이 별로 많지 않아 완전히 균등하게 나누어지지는 못한 것 같습니다.

 

customer_df['frequency_grade'].value_counts()

 

Frequency도 등급별 매출 기여도를 파이 차트로 그려 봅시다. Recency 때와 비슷하게 frequency_grade 컬럼에 대해 groupby 한 뒤 sum() 함수를 사용할게요. 이 groupby 결과의 amount_total 컬럼에 대해 파이 차트를 그리면 되겠습니다. 시각화한 걸 보니 매출 기여도는 3등급, 2등급, 1등급 순으로 높군요. 3등급과 2등급의 매출 기여도 차이는 그리 크지 않고, 둘에 비해 1등급의 매출 기여도가 상당히 낮습니다. 1등급 고객의 수가 더 많은데도 이런 결과가 나오다니 흥미롭네요.

 

groupby_frequency_grade = customer_df.groupby('frequency_grade').sum(numeric_only=True).reset_index()
groupby_frequency_grade['amount_total'].plot(
    kind='pie',
    autopct='%.1f%%',
    labels=[f'{x}등급' for x in grade_labels],
    title='Frequency 등급별 매출 기여도',
    ylabel='',
)

 

마지막으로 Monetary 등급을 구할 차례입니다. 이번에는 amount_total 컬럼을 구간화하면 되겠죠. Monetary는 구매 금액을 뜻하므로 Frequency와 마찬가지로 숫자가 클수록 더 높은 등급이 매겨져야 할 거예요. qcut() 함수로 등급을 나눈 뒤 customer_dfmonetary_grade라는 컬럼으로 추가하겠습니다.

 

customer_df['monetary_grade'] = pd.qcut(
    x=customer_df['amount_total'], q=num_grades, labels=grade_labels
)

 

monetary_grade 컬럼에 value_counts() 함수를 적용해 보면 각 등급별로 고객 수가 정확히 균등하게 분배되었음을 알 수 있습니다.

 

customer_df['monetary_grade'].value_counts()

 

이제 Monetary 등급에 따라 매출 기여도가 어떻게 다른지 파이 차트로 그려 볼게요. 결과를 보면 3등급의 매출 기여도가 매우 높고 1등급의 매출 기여도는 아주 낮게 나오네요. 아무래도 Monetary 등급은 돈을 얼마나 썼는지와 직접적으로 관련되기 때문에 그만큼 매출 기여도가 명확히 구분되는 것 같습니다.

 

groupby_monetary_grade = customer_df.groupby('monetary_grade').sum(numeric_only=True).reset_index()
groupby_monetary_grade['amount_total'].plot(
    kind='pie',
    autopct='%.1f%%',
    labels=[f'{x}등급' for x in grade_labels],
    title='Monetary 등급별 매출 기여도',
    ylabel='',
)

 

 

가중합을 이용해 RFM 고객 세그먼트 분류하기

 

Recency, Frequency, Monetary 각 항목의 등급을 모두 매겼으니 이제 이를 활용해 고객 지표를 구하고 고객 세그먼트 분류까지 해 볼게요. 먼저 Recency, Frequency, Monetary 등급 각각에 가중치를 곱한 뒤 더하면 고객 지표가 계산됩니다. 이 고객 지표를 몇 가지 등급으로 나누면 최종 고객 세그먼트를 구할 수 있죠. 그럼 RFM 고객 세그먼트의 등급 개수도 3개로 두고 진행해 보겠습니다. 혹시 RFM 분석 방법이 잘 기억나지 않는다면 여기를 참고해 주세요.

일단 세 항목에 동일하게 가중치를 줄 때 고객 세그먼트가 어떻게 나오는지 확인해 볼까요? 세 항목에 대한 가중치에는 조건이 있었죠. 각각 0보다 크거나 같아야 하고 모두 더했을 때 1이 되어야 합니다. 가중치 조건을 고려했을 때 각 항목에 1/3씩 가중치를 부여하면 되겠군요. 가중치는 weight라는 이름의 딕셔너리 변수를 이용해 관리하겠습니다.

 

weight = {}
weight['recency'] = 1 / 3
weight['frequency'] = 1 / 3
weight['monetary'] = 1 / 3

 

여기서 주의할 점이 하나 있는데요. customer_df에 Recency, Frequency, Monetary 각 등급이 숫자 1, 2, 3으로 표시되어 있지만, 엄밀히 말하면 등급 값은 모두 숫자형 데이터가 아니라 범주형 데이터인 상태입니다. 가중치를 적용하는 연산을 하려면 astype() 함수를 이용해 숫자형 데이터로 바꿔 줘야 해요. 어차피 등급 값은 1, 2, 3 뿐이니 정수형으로 바꿔도 무방하겠습니다. 그럼 항목에 맞게 가중치와 등급을 곱한 뒤 더해 줍시다. 이렇게 구한 고객 지표는 customer_dfrfm_score 컬럼으로 추가할게요.

 

customer_df['rfm_score'] = (
    weight['recency'] * customer_df['recency_grade'].astype('int')
    + weight['frequency'] * customer_df['frequency_grade'].astype('int')
    + weight['monetary'] * customer_df['monetary_grade'].astype('int')
)

 

이제 고객 지표를 세 등급으로 구간화하면 고객 세그먼트 분류를 마칠 수 있습니다. 고객 지표는 그 범위가 1 이상 3 이하일 수밖에 없는데요. Recency, Frequency, Monetary 모두 등급 값이 1, 2, 3으로 한정되어 있고, 각 가중치는 모두 더해서 1이라는 제약 조건까지 있기 때문입니다. 최솟값과 최댓값이 확실하게 정해져 있으니 등급마다 고객 수를 균등하게 분배하기보다는, 최솟값과 최댓값을 고려하여 동일한 크기의 구간으로 고객을 분류해 볼게요. 아래와 같이 구간을 정하면 세 구간의 크기가 모두 2/3가 되겠죠? 물론 구간 경계에서 이상과 초과, 이하와 미만의 차이 때문에 2등급 고객의 수가 더 많아질 가능성이 있긴 합니다.

 

고객 지표 구간 등급
1 이상 5/3 미만 1등급
5/3 이상 7/3 이하 2등급
7/3 초과 3 이하 3등급

 

이렇게 구간 경계의 포함 여부가 일관되지 않은 경우에는 cut() 함수로 구간화를 할 수가 없습니다. 그래서 이번에는 apply() 함수를 이용해 구간화를 해 볼게요. 먼저 rfm_score 값을 받으면 위의 기준에 따라 RFM 세그먼트를 리턴해 주는 함수 rfm_segment_bins()를 정의합시다.

 

def rfm_segment_bins(x):
    if x < 5 / 3:
        return 1
    elif x <= 7 / 3:
        return 2
    else:
        return 3

 

다음으로 rfm_score 컬럼 데이터에 대해 apply() 함수를 호출하면 되는데요. rfm_segment_bins() 함수의 이름을 입력으로 넣어 주어야 합니다. 그러면 rfm_score 컬럼의 값 하나하나가 rfm_segment_bins() 함수를 통과한 값으로 바뀌어 의도한 대로 세그먼트를 나눌 수 있어요. apply() 함수의 결과는 rfm_segment이라는 이름의 새로운 컬럼을 만들어 customer_df에 추가해 줄게요.

 

customer_df['rfm_segment'] = customer_df['rfm_score'].apply(rfm_segment_bins)

 

구간화를 마친 뒤 rfm_segment 컬럼에서 value_counts() 함수를 호출하여 세그먼트마다 고객이 몇 명씩 있는지 확인해 봅시다. 출력된 결과를 보니 1등급에서 3등급으로 갈수록 고객 수가 줄어드네요.

 

customer_df['rfm_segment'].value_counts()

 

그럼 고객 세그먼트별로 매출 기여도가 어떻게 다른지 살펴봅시다. 이번에는 rfm_segment 컬럼을 기준으로 groupby 해 준 뒤 파이 차트를 그려야겠군요. 결과를 보면 1등급 세그먼트 고객은 확실히 매출에 적게 기여하고 있습니다만, 2등급과 3등급의 매출 기여도 차이가 그리 크지 않습니다. 매출 기여도가 더 확연히 높은 세그먼트가 있어야 그 고객들에게 집중할 수 있을 텐데 말이죠. 가중치를 변경하여 다시 고객 지표를 계산하고 고객 세그먼트까지 나누어 봐야겠습니다.

 

groupby_rfm_segment = customer_df.groupby('rfm_segment').sum(numeric_only=True).reset_index()
groupby_rfm_segment['amount_total'].plot(
    kind='pie',
    autopct='%.1f%%',
    labels=[f'{x}등급' for x in grade_labels],
    title='RFM 고객 세그먼트별 매출 기여도',
    ylabel='',
)

 

앞선 분석에서 Recency 등급은 등급별로 매출 기여도 차이가 거의 없었는데요. 그렇다면 Recency 가중치를 줄이고 다른 항목의 가중치를 높였을 때, 고객 세그먼트별 매출 기여도의 차이가 더 뚜렷해질 것이라고 추측할 수 있습니다. Recency 가중치를 0.2로 줄이고 다른 두 가중치는 동등하게 0.4씩 설정한 뒤 같은 과정을 반복해 봅시다.

 

# 가중치 재설정
weight['recency'] = 0.2
weight['frequency'] = 0.4
weight['monetary'] = 0.4

# 가중합 계산
customer_df['rfm_score'] = (
    weight['recency'] * customer_df['recency_grade'].astype('int')
    + weight['frequency'] * customer_df['frequency_grade'].astype('int')
    + weight['monetary'] * customer_df['monetary_grade'].astype('int')
)

# RFM 고객 지표 구간화
customer_df['rfm_segment'] = customer_df['rfm_score'].apply(rfm_segment_bins)

 

새로운 가중치로 고객 세그먼트를 나누니 이전 가중치로 나눴을 때에 비해 1등급과 2등급 세그먼트 고객의 수는 줄고 3등급 세그먼트 고객의 수가 많이 늘었네요.

 

customer_df['rfm_segment'].value_counts()

 

세그먼트별 매출 기여도를 파이 차트로 그려 보니 3등급 세그먼트의 매출 기여도가 70%에 육박합니다. 2등급 세그먼트는 27% 정도, 1등급 세그먼트는 4%로, 3등급 세그먼트의 매출 기여도가 확연히 높군요. 매출 기여도가 높은 3등급 세그먼트를 핵심 고객 그룹으로 생각하고 집중적으로 관리해 주면 앞으로도 매출에 도움이 되겠죠?

 

groupby_rfm_segment = customer_df.groupby('rfm_segment').sum(numeric_only=True).reset_index()
groupby_rfm_segment['amount_total'].plot(
    kind='pie',
    autopct='%.1f%%',
    labels=[f'{x}등급' for x in grade_labels],
    title='RFM 고객 세그먼트별 매출 기여도',
    ylabel='',
)

 

 


 

4. 세그먼트별 특성 및 소비 성향 분석

세그먼트마다 연령대 분포 파악하기

Recency, Frequency, Monetary 가중치를 각각 0.2, 0.4, 0.4로 설정하여 구한 RFM 고객 세그먼트를 바탕으로 세그먼트별 고객 특성과 소비 성향을 분석해 보겠습니다.

먼저 세그먼트별로 연령대 분포가 어떻게 다른지 확인해 볼까요? 각 세그먼트마다 연령대 분포를 파이 차트로 시각화하면 한눈에 파악이 될 텐데요. 이를 위해서는 고객 세그먼트와 연령대 두 요소를 기준으로 고객 수를 집계해야 합니다. 그러면 일단 rfm_segmentage_group 두 컬럼에 대해 groupby를 해 줘야겠죠. 그다음 size() 함수를 사용하면 고객의 수를 얻을 수 있습니다.

 

groupby_rfm_segment_age_group = customer_df.groupby(['rfm_segment', 'age_group']).size().reset_index()
groupby_rfm_segment_age_group

 

고객의 수가 표시된 컬럼 이름이 그냥 0이라 써져 있어서 컬럼 이름을 num_customers라고 바꿔 볼게요. 데이터 전처리 과정에서 했듯 rename() 함수를 사용합시다. columns 파라미터에 딕셔너리를 전달할 때, 키 값인 0에 따옴표가 붙지 않는다는 점을 주의해 주세요.

 

groupby_rfm_segment_age_group = groupby_rfm_segment_age_group.rename(columns={0: 'num_customers'})

 

이제 파이 차트를 그리면 됩니다. for 문을 이용해 고객 세그먼트를 순회할 건데요. 각 세그먼트에 해당하는 데이터를 불린 인덱싱으로 추출한 뒤, num_customers 컬럼을 plot() 함수로 시각화하겠습니다. 이번에는 labels 파라미터에 변수를 따로 만들어서 넣어 주지 않고 age_group 컬럼의 unique 값을 전달할게요.

 

for i_segment in range(1, num_grades + 1):
    age_group_dist = groupby_rfm_segment_age_group[
        groupby_rfm_segment_age_group['rfm_segment'] == i_segment
    ]
    age_group_dist['num_customers'].plot(
        kind='pie',
        autopct='%.1f%%',
        labels=age_group_dist['age_group'].unique(),
        title=f'{i_segment}등급 세그먼트 연령대별 고객 분포',
        ylabel='',
    )
    plt.show()

 

1등급 세그먼트 고객에 비해 2등급과 3등급 세그먼트 고객은 30대 이하의 비중이 작습니다. 한편 2등급 세그먼트와 3등급 세그먼트는 각 연령대마다 비율이 2% 포인트 내외만 차이 날 정도로 연령대 분포가 비슷한 경향을 띠고 있습니다. 2등급보다 3등급 세그먼트에서 30대 이하 비중이 약간 더 작긴 하네요. 일반적으로 중장년층에 비해 청년층의 구매력이 더 떨어지기 때문에 이런 결과가 나온 것 같습니다. 매출 기여도가 높은 3등급 세그먼트에 집중하여 마케팅을 전개한다면 젊은 세대보다는 중장년층을 타깃으로 하는 게 효과가 더 클 가능성이 높겠어요.

 

 

세그먼트마다 가족 구성 분포 파악하기

 

연령대 분포를 파악했던 것과 같은 방식으로 고객 세그먼트마다 가족 구성이 어떻게 다른지 확인해 보겠습니다. 가족 구성과 관련된 컬럼은 혼인 상태를 나타내는 marital_status와 부양 자녀 수를 나타내는 children이 있습니다. 이 두 컬럼을 하나씩 살펴볼게요.

먼저 세그먼트마다 혼인 상태별로 고객 수가 어떻게 분포되어 있는지 보겠습니다. rfm_segment, marital_status 두 컬럼에 대해 groupby를 수행하고 size() 함수를 이어서 호출해 주면 되겠죠? for 문을 이용해 각 세그먼트마다 파이 차트를 그릴게요.

 

groupby_rfm_segment_marital = customer_df.groupby(['rfm_segment', 'marital_status']).size().reset_index()
groupby_rfm_segment_marital = groupby_rfm_segment_marital.rename(columns={0: 'num_customers'})

for i_segment in range(1, num_grades + 1):
    marital_status_dist = groupby_rfm_segment_marital[
                groupby_rfm_segment_marital['rfm_segment'] == i_segment
        ]
    marital_status_dist['num_customers'].plot(
        kind='pie',
        autopct='%.1f%%',
        labels=marital_status_dist['marital_status'].unique(),
        title=f'{i_segment}등급 세그먼트 혼인 상태별 고객 분포',
        ylabel='',
    )
    plt.show()

 

혼인 상태의 분포는 세그먼트에 따라 큰 차이가 없네요. 모든 세그먼트에서 배우자와 함께 사는 고객의 비율이 65% 정도로 높게 나타나며, 그다음으로 미혼자 비율이 20% 내외인 걸 확인할 수 있습니다.

그럼 세그먼트마다 부양 자녀의 수는 어떻게 분포되어 있는지 살펴봅시다. 이번에는 rfm_segment, children 두 컬럼에 대해 groupby 한 뒤 size() 함수를 호출해야겠네요. 이전과 마찬가지로 for 문을 통해 세그먼트를 순회하며 파이 차트를 그릴게요.

 

groupby_rfm_segment_children = customer_df.groupby(['rfm_segment', 'children']).size().reset_index()
groupby_rfm_segment_children = groupby_rfm_segment_children.rename(columns={0: 'num_customers'})

for i_segment in range(1, num_grades + 1):
    children_dist = groupby_rfm_segment_children[
                groupby_rfm_segment_children['rfm_segment'] == i_segment
        ]
    children_dist['num_customers'].plot(
        kind='pie',
        autopct='%.1f%%',
        labels=[f'{i}명' for i in children_dist['children'].unique()],
        title=f'{i_segment}등급 세그먼트 부양 자녀 수별 고객 분포',
        ylabel='',
    )
    plt.show()

 

부양 자녀 수는 세그먼트별로 뚜렷한 차이를 보이고 있습니다. 1등급에서 3등급으로 올라갈수록 자녀가 없는 고객의 비중이 상당히 커지며, 자녀가 있는 고객의 비중은 자녀 수와 무관하게 모두 비중이 줄어들고 있어요. 3등급 세그먼트에는 무자녀 고객이 40%를 넘으니 이러한 특성을 마케팅 전략에 반영할 수도 있겠네요.

 

 

세그먼트마다 품목별 매출 기여도 파악하기

 

이번에는 고객 세그먼트마다 품목별 매출 기여도가 어떻게 분포되어 있는지 살펴봅시다. 먼저 rfm_segment를 기준으로 groupby 해 줘야겠죠. 그다음에는 품목별로 매출 합을 구해야 하니 sum() 함수를 호출하겠습니다.

 

groupby_rfm_segment = customer_df.groupby('rfm_segment').sum(numeric_only=True).reset_index()

 

파이 차트를 그리기 위해 groupby 결과로부터 세그먼트 등급과 품목별 매출이 담긴 데이터만 뽑아 보겠습니다. 먼저 타깃이 되는 컬럼 이름들을 리스트 변수로 만들어 줄게요. 당연히 rfm_segment는 포함되어야 할 테고 amount_로 시작하는 컬럼까지 고르면 되는데요. 지금은 품목별 매출만 필요하기 때문에 모든 품목의 매출을 합한 amount_total 컬럼은 제외하겠습니다.

일단 list comprehension을 통해 amount_total을 제외하고 이름이 amount_로 시작하는 컬럼 이름들의 리스트부터 만들어 봅시다. for 문으로 groupby_rfm_segment.columns를 순회하면서 조건에 부합하는 컬럼 이름을 그대로 리스트에 넣어 주면 됩니다. List comprehension에서는 if 문을 for 문 뒤에 써 주면 조건을 설정할 수 있어요. startswith() 함수를 통해 이름이 amount_로 시작하는지 판단하고 동시에 amount_total과 이름이 다른지도 체크하겠습니다. List comprehension으로 리스트를 만들고 나면 append() 함수를 이용해 rfm_segment 까지 리스트에 추가해 줄게요. 이 리스트 변수의 이름은 selected_columns라고 하겠습니다.

 

selected_columns = [
    col
    for col in groupby_rfm_segment.columns
    if col.startswith('amount_') and col != 'amount_total'
]
selected_columns.append('rfm_segment')
selected_columns

 

이 컬럼들에 해당하는 데이터만 떼서 amount_sum_per_product라는 변수에 넣어 줄게요. 그리고 set_index() 함수를 통해 rfm_segment 컬럼을 인덱스로 만들겠습니다.

 

amount_sum_per_product = groupby_rfm_segment[selected_columns]
amount_sum_per_product = amount_sum_per_product.set_index('rfm_segment')
amount_sum_per_product

 

이번에도 역시 for 문을 이용해 고객 세그먼트를 순회하며 그래프를 그리겠습니다. 앞서 rfm_segment 컬럼을 인덱스로 바꿔 두었기 때문에 loc을 통해 각 세그먼트별 데이터에 접근할 수 있어요. plot() 함수로 그래프를 그려 봅시다. 파라미터 설정은 모두 익숙할 거예요. 품목 순서에 맞게 labels에 품목 이름을 잘 전달해 주면 그래프를 파악하기 더 쉽겠죠?

 

for i_segment in range(1, num_grades + 1):
    amount_sum_per_product.loc[i_segment].plot(
        kind='pie',
        autopct='%.1f%%',
        labels=['주류', '과일', '육류', '수산물', '과자', '잡화'],
        title=f'{i_segment}등급 세그먼트 품목별 매출 기여도',
        ylabel='',
    )
    plt.show()

 

모든 고객 세그먼트에서 공통적으로 주류 매출이 가장 높고 그다음으로 육류의 매출이 높네요. 주류는 세그먼트에 따라 뚜렷하게 매출 기여도가 달라지는 품목이기도 합니다. 1등급에서 3등급으로 갈수록 주류 매출의 비중이 높아지고 있어요. 잡화 또한 경향성이 뚜렷한데요. 주류와 반대로 1등급에서 3등급으로 갈수록 매출 기여도가 큰 폭으로 감소하고 있습니다. 한편 수산물, 과일, 과자 모두 1등급에서 3등급으로 갈수록 조금씩 비중이 줄어듭니다. 육류의 매출 기여도는 3등급보다 2등급에서 약간 높게 나타나는군요.

이렇게 고객 세그먼트마다 품목별로 매출 기여도가 달라지는 이유는 가족 구성과도 관련이 있을 듯합니다. 가족 구성 분석에 따르면 1등급에서 3등급으로 갈수록 혼인 상태에는 큰 변화가 없는데 자녀의 수는 줄어드는 경향이 있었죠. 그러니 주류 소비는 늘고 과일, 과자 등 간식이나 잡화(생필품) 소비가 줄어드는 경향이 나타날 수도 있겠습니다.

어쨌든 3등급 세그먼트는 주류와 육류의 매출 기여도를 합치면 거의 80%에 육박하네요. 따라서 이 두 품목을 중심으로 마케팅을 전개하면 효과가 좋을 것 같습니다. 상황에 따라 3등급 세그먼트의 잡화 매출 기여도를 끌어올려야 한다면 그에 맞게 전략을 짤 수도 있고요.

 

 

세그먼트마다 프로모션 참여율 파악하기

 

마지막으로 고객 세그먼트마다 프로모션 참여율은 어땠는지 살펴봅시다. 프로모션 관련 컬럼을 보면 고객이 프로모션에 참여했을 시 1, 참여하지 않았을 시 0으로 표시되어 있는데요. 그렇기 때문에 rfm_segment로 groupby 한 후 mean() 함수를 써 주면 자연스럽게 세그먼트마다 프로모션별 참여율을 구할 수 있습니다.

 

groupby_rfm_segment = customer_df.groupby(['rfm_segment']).mean(numeric_only=True).reset_index()

 

파이 차트를 그리기 위해 groupby 결과 데이터에서 세그먼트 등급과 프로모션별 참여율이 담긴 데이터만 뽑아 보겠습니다. 먼저 타깃이 되는 컬럼 이름들을 리스트 변수로 만들어 줄게요. promotion_1부터 promotion_6까지는 list comprehension으로 간단히 만들어 주고 여기에 rfm_segment를 추가하겠습니다.

 

selected_columns = [f'promotion_{i}' for i in range(1, 7)]
selected_columns.append('rfm_segment')
selected_columns

 

품목별 매출 기여도를 살펴볼 때처럼 타깃 컬럼의 데이터만 떼서 새로운 변수에 넣어 준 뒤, set_index() 함수를 통해 rfm_segment 컬럼을 인덱스로 만들겠습니다. 변수 이름은 avg_promotion이라고 해 볼게요.

 

avg_promotion = groupby_rfm_segment[selected_columns]
avg_promotion = avg_promotion.set_index('rfm_segment')
avg_promotion

 

세 가지 고객 세그먼트와 여섯 가지 프로모션에 대해 참여율을 한눈에 파악할 수 있는 그래프를 그리고 싶은데요. 세로 방향 막대그래프를 그리되, X축이 고객 세그먼트를 나타내고 Y축에 참여율을 나타내면 좋을 것 같습니다. 여섯 가지 프로모션은 막대 색깔로 구분시키고요. plot() 함수의 kind'bar'를 넣어서 호출하면 그런 그래프를 그릴 수 있습니다. 그래프 제목 및 X축, Y축 이름을 지정하고 격자까지 추가해서 그래프를 그려 볼게요.

 

avg_promotion.plot(kind='bar')
plt.title('고객 세그먼트별 프로모션 참여율')
plt.xlabel('고객 세그먼트')
plt.ylabel('프로모션 참여율')
plt.grid()

 

1등급에서 3등급으로 갈수록 대체로 프로모션 참여율이 눈에 띄게 증가하는 걸 확인할 수 있네요. 특히 1등급 세그먼트 고객 중에 1번 프로모션에 참여한 사람은 아무도 없을 정도예요. 돈을 많이 쓰는 고객일수록 그만큼 평소에 마트 행사에도 관심이 많을 가능성이 커 참여율이 높아지는 게 아닐까 싶습니다. 다만 3번 프로모션은 모든 고객 세그먼트에서 비슷한 참여율을 기록했다는 점이 특이하네요.

그럼 매출 기여도가 높은 3등급 세그먼트 고객을 타깃으로 프로모션을 새롭게 진행할 때 어떤 점을 참고하면 좋을까요? 일단 2번 프로모션은 참여율이 저조했으니 문제점이 무엇이었는지 복기해 볼 수 있겠죠. 한편 1, 4, 5번 프로모션의 참여율이 준수한 편이었고 6번 프로모션의 참여율은 다른 프로모션에 비해 확실히 높았습니다. 이러한 점을 참고하여 프로모션을 기획해 보면 좋을 것 같네요.