본 프로젝트의 목표는 식중독 발생 확률을 예측하는 모델을 구축하여, 일반 시민과 정부 부처가 식중독을 사전에 예방할 수 있도록 지원하는 것이다. 필자는 데이터 분석 및 모델링을 담당하였으며, 주요 역할은 다음과 같다: 1) 데이터 수집 및 전처리: 식품의약품안전처의 Open API 데이터를 활용하여 식중독 현황을 분석하고, 관련 변수를 정리하였다. 2) 가설 설정: 식중독 발생에 지역축제, 사회 인프라가 영향을 미칠 것이라는 가설을 설정하였다. 관련 요인들을 정의하고, 이에 대한 데이터를 수집하여 모델링에 활용하였다. 3. 모델링 및 성과 평가: 여러 분류 모델을 비교하고, Random Forest 모델을 통해 70%대 중후반의 성능을 기록하여 식중독 발생 예측의 정확성을 높였다. 결과적으로, 지역별 식중독 발생 위험을 예측할 수 있는 모델을 성공적으로 구축하였으며, 이를 통해 맞춤형 예방 대책의 필요성이 제기되었다. 향후 통합 데이터와 일별 예측 모델을 통해 더욱 정교한 분석이 가능할 것으로 기대된다.
A. 배경
식중독(food poision)이란 식품위생법 제2조제14항에 따라 식품의 섭취로 인하여 인체에 유해한 미생물 또는 유독 물질에 의하여 발생하였거나 발생한 것으로 판단되는 감영성 또는 독소형 질환을 말한다. 일반적으로 구토, 설사, 발열 등의 증상을 나타내며 원인물질에 따라 잠복기와 증상의 정도가 다르게 나타난다. 비교적 가벼운 증상으로 나타지만, 면연력이 약한 어린이, 노인, 인산부의 경우에는 생명의 위협을 받을 수 있다. 2022년 6월 식품의약품안전처에서 발표한 보도자료에 따르면 2016년부터 2018년까지 식중독으로 인한 연간 손실비용이 연간 1조 8,532억원에 달하며 개인 손실비용이 1조 6,418억원(88.6%)을 차지한다고 밝혔다. 개인의 신체적 고통과 개인 비용 뿐만 아니라 사회적 비용을 줄일 필요성이 존재한다. 이에 이번 프로젝트에서는 기후, 인구밀도 등 다양한 외부 변수를 기반으로 지역별 식중독 발생확률을 예측하는 모델을 구축하고 일반시민 또는 관련 정부부처에서 식중독을 사전에 예방하는데 도움이 되고자 식중독 발생 예측 지도를 생성하고자 한다.
B. 데이터 준비
식품의약처안전처에서 제공하는 Open API 형식의 '지역별','원인물질별' 식중독 현황 데이터를 사용하였다. 지역별 데이터와 원인물질별 데이터를 결합할 수 있는 PK가 없어 정보공개청구 창구를 사용하여 지역별, 원인물질별, 원인시설별 식중독 발생 현황에 대한 자료를 요청하였지만, 식약처의 국정감사기간과 겹치며 기존 자료 수령일보다 10일 연기되어 자료 수령이 제 시간내에 불가능하게 되었다. 따라서 Open API 데이터를 각각 활용하여 2개의 테이블에 대한 데이터 전처리, 모델링 등이 이루어졌다.
식중독 지역별 현황
식중독 원인물질별 현황
OCCRNC_VIRS에서 원인물질은 총 15개로 집계된다. 이 중 의미를 부여할 수 없는 '원인 불명을 비롯하여 발생 비율이 1% 미만인 원인 물질들을 제외하고 9가지 주요 원인 물질을 선택하여 진행하였다.
범례
원인물질별 식중독 발생건수 비율
최종 원인물질별 식중독 발생건수
식중독균별 특성을 이해하고, 그에 따른 발생 확률을 증가시키는 요인에 대해 크게 3가지로 나누어 각 가설을 세워 추가 변수들에 대한 데이터를 수집하였다. 가설과 데이터는 다음과 같다.
요인
가설
관련 데이터
원인물질 발현 증가
고온 다습해질 수록 박테리아성 식중독 발생율은 증가할 것이다.
기후(온도, 습도, 해면 기압 등) 데이터
노출빈도 증가
인구 밀도가 높을 수록 식중독 발생율은 증가할 것이다.
인구수 데이터, 집단급식소 데이터
축제 등 이벤트 발생이 많으면 식중독 발생율은 증가할 것이다.
지역 축제 데이터, 황금연휴 데이터
예방/관리 소홀
사회 인프라가 잘 갖춰지지 않을 수록 식중독 발생율이 증가할 것이다.
소비자물가지수(CPI) 데이터
추가 변수들을 생성할 수 있는 데이터들을 '년', '월' , '지역' 등에 따라 각 데이터 테이블을 생성하였다.
지역별 데이터
원인물질별 데이터
다층분류 분석을 하고자 하였으나, 유의미한 결과가 나타나지 않아 발생 유무를 예측하는 방법으로 프로젝트 방향을 잡고, 이진 변수로 사용할 수 있는 칼럼들은 이진 변수로 사용하였다. 또한, 통계청의 데이터 이슈로 인해 추가 변수들의 데이터를 2022년까지 구할 수 있어 2002년부터 2022년까지 20년간의 데이터를 사용하였다. 세종특별자치시의 경우, 2012년 7월 충청도에서 출범한 지역이다 보니 다른 지역과 다르게 각 부처마다 데이터 적재시점이 다르다. 이에 따라 세종의 날씨 데이터와 CPI 데이터는 정합성과 적합성 평가를 통해 인근지역인 충청북도의 데이터를 이용해 선형회귀 모델로 보완하였다.
C. 데이터 분포 및 기본 특성
아래 그래프에서 볼 수 있듯 지역별, 원인물질별 데이터 불균형이 강하게 나타났다. 모델링 시 이를 고려하여, 오버샘플링(Oversampling) 혹은 SMOTE 기법을 사용할 수 있을 것이다.
지역별 히스토그램
원인물질별 히스토그램
식중독 발생에 대한 기본적인 통계를 살펴보면 다음과 같은 결과를 알 수 있다. 2002년부터 2022년까지 연도별 식중독 발생건수 및 환자 수를 살펴보면 특정 년도에 총환자수가 급증하는 것을 볼 수 있는데, 발생 건수 또한 급증한 것은 아니기에 대량 감염 사건이 발생했을 것이라 예상할 수 있다.
연도별 식중독 발생건수로 2018년 급증하는 환자수와 2020년 급락하는 환자수를 볼 수 있다.
예상과 같이 2018년 학교 급식에서 초코케이크 살모넬라 식중독으로 인해 약 2,200여명의 대규모 환자가 발생한 사건이 있었다. 또한, 2020년에는 COVID-19로 인한 보건정책 및 위생관리 강화로 환자수가 전년대비 36.88%나 감소하는 결과를 보인다.
연도별 식중독 발생건수 대비 환자 수
연도별 원인물질별 식중독 발생건수 대비 환자 수
연도별 지역별 발생 건수 변화
원인물질별 발생건수 대비 환자 수 비율
월별 원인물질별 총 발생 건수
2000년대 초반 이후 식중독 발생건수가 점점 줄어 들며, 전국적으로 발생 건수가 적었다. 2018년 일명 초코 케이크 사건이라 불리는 식중독 집단 감염 사건의 경우, 전국적으로 살모넬라균으로 인해 발생하였기 때문에 해당 시기에 급증한 그래프를 확인할 수 있다. 월별 식중독균별 발생건수를 살펴보면, 노로바이러스는 겨울철 정점에 도달하였다 점점 감소하여 여름에 최저 발생 건수를 기록하는 양상을 보인다. 반면, 병원성 대장균, 살모넬라, 그리고 캠필로박터제주니는 봄부터 증가하여 여름철에 최다 발생률을 보인다. 각 원인물질별에 따라 계절성이 나타난다는 것으로 해석할 수 있다. 지역별로 살펴보았을 경우, 서울-경기권이 전체 식중독 발생 건수의 35% 가량을 차지하였으며, 그 이후로는 부산, 경남, 강원, 인천 등의 순으로 나타났다. 서울-경기권이 타 지역에 비해 발생건수가 지속적으로 높게 나타나는 것을 알 수 있다.
D. 식중독 발생 확률 예측 모델링
분류 예측 모델링 워크 플로우
PyCaret을 활용하여 다양한 분류모델의 기본 성능을 평가하였다. 초기 모델 성능을 확인하고, 성능 개선을 목표로 4개의 후보 모델을 선정하였다. 지역별, 원인물질별 초기 좋은 성능을 보인 모델은 Random Forest, Gradient Boosting, LightGBM, XGBoost 이었다. 이 모델들은 기본적으로 성능 지표가 높게 나오기도 하였으나 비선형 패턴을 학습하는 능력, 변수 중요도 파악, 그리고 과적합을 효과적으로 제어할 수 있다.
본 프로젝트의 특이점은 일반적인 모델 성능평가 지표인 Accuracy 혹은 F1-Score보다 Recall(재현율)을 우선시하여 모델 성능을 평가하였다. Recall은 실제 양성 사례를 놓지지 않고 정확히 예측하는 능력을 나타내는 지표로, 특히 헬스케어 분야, 바이오 분야에서는 질병 진단이나 이상 징후를 조기에 감지하는데 중요한 역할을 한다. 우리 프로젝트의 주요 목적은 식중독 발생 가능성을 정확히 예측하여 사전에 예방 조치를 취할 수 있도록 하는 것이다. Recall 지표는 발생하지 않을 확률보다 발생할 확률을 잘 예측함으로 모델 성능을 평가하는데 적합하다고 판단할 수 있다.
먼저 선형회귀 모델 가능 여부 판단을 위해 선형회귀 모형의 기본가정인 선형성, 정규성, 등분산성, 독립성을 검토해본 결과, 사피로 검정에서 p-value 값이 0.05보다 작아서 정규성을 충족하지 않는 것으로 나타났으며, 다른 항목에서도 완전한 충족이 이루어지지 않아 비선형회귀모형, 시계열 모형, 분류 모형 등 다양한 모형을 시도하였다.
선형회귀모델 가능여부 검토
MinMax Scaler를 이용한 표준화 및 종속변수 BoxCox 변환
Random OverSampling 적용
회귀모형과 분류모형은 각 발생 건수와 식중독 발생여부를 종속변수로 모델링하였다. 처음 Train Data, Test Data로 분리시키고, MinMax Scaler와 BoxCox 변환을 통해 이상치의 영향은 줄이고 변수들 간의 비선형성을 개선하고자 하였다. 또한, 분류모델에서는 발생 여부를 나타내는 0과 1의 불균형으로 인해 모델이 하나의 결과에 편향되는 것을 방지하고자 SMOTE, Borderline-SMOTE 등 다양한 OverSampling 기법을 이용해 보았으나 Random OverSampling의 결과가 가장 좋았다. 이 후 GridSearchCV를 통해 모델별 validation data 기준 성능이 가장 우수한 하이퍼 파라미터를 선택하여 모델링을 실시하였다. 모델의 결과는 아래와 같다.
각 분석별 모델를 가장 설명하는 기법 선택
보시다시피 회귀모형, 시계열 모형, 딥러닝까지 다양한 모델들의 성능 평가 결과를 살펴보았을 때 분류모형을 제외한 다른 모형들 중 다중회귀 모형이 가장 좋은 성능을 보였다. 하지만 이 경우 최대 R2 score는 약 0.4로 전반적인 모델 성능이 모두 낮았다. 반면, 분류모형 중 가장 좋은 성능을 보인 Random Forest 분류모형은 최대 Accuracy 0.78로 양호한 성능을 보였다. 분류모형의 경우 기본 모형과 하이퍼 파라미터 튜닝모형의 성능 차이가 크지 않아 하이퍼 파라미터 튜닝 대신 데이터 분할 후 개별 모델링을 검토하였다.
지역별 식중독 발생 예측 모델링 결과원인물질별 식중독 발생 예측 모델링 결과
각 지역별로 예측모델링을 구축하였고, Oversampling, Feature Engineering, Hyperparameter tuning을 통해 70%대 중후반에서 90%대까지 성능이 제법 향상된 것을 확인할 수 있다. 다만, 광주, 세종과 같은 일부 지역은 데이터의 불균형이 너무 심하기 때문에 Oversampling을 하고 tuning을 해줘도 성능 향상에 한계가 있다.
변수 미래값 예측 모델 (Prophet 모델)
지역별 모델링과 동일하게 원인물질별 및 변수별로도 예측모델 구축을 완료하였다. 특히 변수의 미래 예측값은 모두 시계열 모델인 Prophet으로 모델링하였다. 시계열모델인 만큼 '유치원생의 비율', '소비자물가지수'와 같이 변동값이 크지 않은 변수들과 '평균온도', ' 해상기압', '습도'와 같이 계절성을 나타내는 변수의 R2 score 값이 대부분 1에 가깝게 몰려 있음이 확인된다. 그러나 황금연휴와 같이 특별한 주기성을 띄지 않는 변수들은 낮은 R2 score 값에 분포되어 있는 것으로 확인되었다. 해당 값들은 feature importance에서 비교적 낮은 중요도로 측정되는 변수로 모델 성능 저하에 지대한 영향을 미칠 것이라 확언하기 어렵다.
2023년 식중독 발생 예측 흐름
앞서 구축한 모델을 바탕으로 2023년 식중독 발생 확률을 예측하였다. 시계열 모델을 활용하여 2023년 1월부터 12월까지의 변수들의 값을 예측하여 생성하였다. 변수 예측값을 각각 지역별 모델과 물질별 모델에 입력하여 지역별 및 원인물질별 식중독 발생 예측 결과를 얻었다. 이미 2023년의 식중독 발생 기록을 보유하고 있기 때문에 예측 결과와 실제 발생 기록을 비교할 수 있다.
예측값 ROC Curve
통합된 예측값에 대한 ROC Curve를 도출한 결과 지역별 예측 모델의 Recall 값은 0.71, AUC는 0.72 값이 산출되었다. 원인물질별 예측 모델의 경우 Recall 값은 반올림하여 0.77, AUC 값은 0.74로 기록된다. AUC값은 실제 양성과 실제 음성을 잘 구별할 수 있다는 의미로, 모델이 얼마나 잘 예측하였는지를 나타내는 지표이다.
Open API에서 얻은 데이터 자체가 발생 식중독 건수를 월 단위로 집계하여 제공하고 있어 데이터의 n 수가 적고, 정확한 분석에 한계가 있다. 해당 상황에서 나름 준수한 성능을 보였다고 판단된다.
D. 인사이트
변수 중요도를 활용한 원인물질별 발생 위험 지역
분석 모형에서 추출한 모형별 변수 중요도(feature importance)를 이용하여 원인물질별 발생 위험 지역을 식별하였다. 모형별 상위 5대 중요변수가 유사한 지역과 원인물질을 서로 결합한 결과 부산은 병원성 대장균, 경상남도는 황색포도상구균과 유사성을 보였다. 해당 지역은 유사성을 보인 원인물질에 따라서 맞춤형 예방대책이 필요할 것이라고 진단할 수 있다. 다만, 이 시점에서 해당 모델이 지역, 시설, 원인물질 정보가 통합되어 있는 데이터셋을 기반으로 만들어진 것이 아니고, n수가 적다는 사실을 다시금 상기시킬 필요가 있다. 향후 통합데이터, 더 많은 정보를 담고 있는 데이터셋을 이용한다면 일별 데이터 예측하고, 추가 별수들의 일별 데이터를 적용하여 더욱 정교한 모델이 만들어 것이라 기대한다. 아울러 지금처럼 지역과 원인물질별 식중독 발생 예측 결과를 별개의 분석결과로 보는 것이 아닌, 서로 연동되어 '특정 월 또는 특정 환경에서는 어느 지역에서 어떤 원인물질의 발생확률이 증가한다.'와 같이 더욱 깊이 있는 예측 결과를 도출할 수 있을 것이다. 더 나아가 '그 지역에서 어떤 방법을 취해야한다.'와 같은 추가적인 인사이트를 유저들에게 제공할 수 있으리라 기대된다.
E. 대시보드 제작
이와 관련한 예측지도 대시보드를 제작하였다. 일반 국민, 보건 의료 실무자 및 관리자, 보건의료 정책 입안 담당자를 대상으로 현황 모니터링, 시뮬레이션 분석, 향후 12개월 예측, 총 3가지의 기능을 넣었다. 효과적인 정보전달을 위해 식중독 지도, 원인물질별 식중독 막대그래프, SHAP을 활용한 식중독 주요 요인 그래프, 식중독 추이 및 예측값 비교 선그래프 등을 사용하였으며 간결하고 명확한 정보 제공에 주안점을 두었다.
택시기사들에게 전달할 파일로, 코로나와 같은 팬데믹 상황이 왔을 때 대비할 수 있게 정보를 전달하고자 한다 상세: 2019년부터 2024년까지 매년 12월 데이터를 통해 뉴욕 옐로우 택시 이용 패턴을 확인하여 택시 기사들의 수익 극대화를 도모한다.
B. 데이터 선택
뉴욕은 미국의 최대 도시이자 가장 유명한 관광지이다. 뉴욕의 슬로건이 ‘The City that Never Sleeps’ 절대 잠들지 않는 도시인 것만 보아도 한눈에 알 수 있다. 어떤 계절이건 늘 관광객으로 가득하다. 특히 12월의 뉴욕은 크리스마스를 보내는 사람들, 새해 카운트를 세고자 하는 사람들로 가득찬다. 특정 기간, 시간에 많은 사람들이 모이는 곳에서의 택시 운행 행태를 파악하기 위하여 2019년부터 2023년까지 5개년치의 12월 데이터를 선택하였다.
세부사항: NYC 에서 제공하는 택세 데이터 중 예약없이 길거리에서 잡아 탑승할 수 있는 옐로우 택시 데이터를 기준으로 분석하고자 한다
C. 데이터 전처리
기초 통계량 및 각 데이터 별 수치를 살펴보고 이상치, 결측치 값 처리 방법을 정하였다.
기준: 각 년도 12월 데이터가 아닌 값, 승객 및 여행 정보에서 딕셔너리에 기재되어 있지 않은 값 처리, 결제 및 요금의 음수값 처리 및 이에 따른 총 결제액 재계산. 등등
1. 1차 전처리 코드
import pandas as pd
# 파일 경로 리스트
file_paths = [
'yellow_tripdata_2019-12.parquet',
'yellow_tripdata_2020-12.parquet',
'yellow_tripdata_2021-12.parquet',
'yellow_tripdata_2022-12.parquet',
'yellow_tripdata_2023-12.parquet'
]
# 파일명에 해당하는 연도 리스트
years = [2019, 2020, 2021, 2022, 2023]
# 전처리 함수 정의
def preprocess_and_save(df, year):
# 2023년 파일일 경우 'Airport_fee' 컬럼명을 'airport_fee'로 변경
if year == 2023:
df.rename(columns={'Airport_fee': 'airport_fee'}, inplace=True)
# 컬럼 타입 변경
df['tpep_pickup_datetime'] = pd.to_datetime(df['tpep_pickup_datetime'])
df['tpep_dropoff_datetime'] = pd.to_datetime(df['tpep_dropoff_datetime'])
# 연도 필터링: tpep_pickup_datetime과 tpep_dropoff_datetime 모두 해당 연도에 맞는 데이터만 남기기
df = df[(df['tpep_pickup_datetime'].dt.year == year) & (df['tpep_dropoff_datetime'].dt.year == year)]
# 결측치 처리
df = df.dropna(subset=['passenger_count', 'RatecodeID', 'store_and_fwd_flag', 'congestion_surcharge'])
df = df[df['payment_type'] != 0]
# 승객 및 여행 정보 이상치 전처리
df = df[df['VendorID'] != 5]
df = df[df['tpep_pickup_datetime'].dt.month == 12]
df = df[df['tpep_dropoff_datetime'].dt.month == 12]
df['passenger_count'] = df['passenger_count'].apply(lambda x: 4 if x >= 5 else x)
df = df[df['trip_distance'] >= 0]
# 결제 및 요금 정보 전처리
df = df[df['payment_type'].isin([1, 2])]
df = df[df['total_amount'] > 0]
df['mta_tax'] = 0.5
df['improvement_surcharge'] = 0.3
df['congestion_surcharge'] = df['congestion_surcharge'].apply(lambda x: 2.5 if x == -2.5 else x)
df = df[df['fare_amount'] > 0]
df['extra'] = df['extra'].abs()
# 승객수 0명인 경우 평균값 2명으로 대치
df['passenger_count'] = df['passenger_count'].replace(0, 2)
# RatecodeID 값이 6 이하인 것만 유지
df = df[df['RatecodeID'] <= 6]
# tip_amount: 0 이상인 값만 유지
df = df[df['tip_amount'] >= 0]
# tolls_amount: 0 이상인 값만 유지
df = df[df['tolls_amount'] >= 0]
# total_amount 값 변환
df['total_amount'] = df['fare_amount'] + df['extra'] + df['mta_tax'] + df['improvement_surcharge'] + df['tolls_amount'] + df['congestion_surcharge'] + df['airport_fee'].fillna(0)
# CSV 파일로 저장
csv_file_name = f'D:/Bootcamp/3rd_project/yellow_taxi_data_{year}.csv'
df.to_csv(csv_file_name, index=False)
# 저장 완료 메시지 출력
print(f"CSV 파일 저장 완료: '{csv_file_name}'")
# 모든 파일에 대해 전처리 및 저장 실행
for file_path, year in zip(file_paths, years):
df = pd.read_parquet(file_path)
preprocess_and_save(df, year)
2. 문제 사항 발견
1) trip distance 값과 total amount 값에 처리되지 않은 이상치 발견
실제값에 매우 큰 값들이 있고, 데이터는 제대로 예측하지 못함.
2) Trip distance 값, Total amount 값에서 극단값 발견
위치 정보 전처리 방법 가장 멀리 있는 레코드 두개의 거리 측정 후 그 거리 이상의 값 삭제 처
해당 방법으로 삭제 처리하기 위하여 NYC에서 제공하는 Yellow Taxi Zone 데이터 활용
기존 데이터와 zone의 위도,경도 데이터를 병합한 후 승차 위치를 기준으로 지리적 거리를 측정하였다.
# LocationID와 위도/경도 정보가 들어있는 location_df 로드
location_df = pd.read_csv('taxi_zones_latlong.csv') # LocationID, Latitude, Longitude가 포함된 데이터
# PULocationID를 기준으로 위도와 경도를 병합 (픽업 지점)
df = df.merge(location_df[['LocationID', 'Latitude', 'Longitude']], left_on='PULocationID', right_on='LocationID', how='left')
df.rename(columns={'Latitude': 'PULatitude', 'Longitude': 'PULongitude'}, inplace=True)
df.drop(columns=['LocationID'], inplace=True)
# DOLocationID를 기준으로 위도와 경도를 병합 (드롭오프 지점)
df = df.merge(location_df[['LocationID', 'Latitude', 'Longitude']], left_on='DOLocationID', right_on='LocationID', how='left')
df.rename(columns={'Latitude': 'DOLatitude', 'Longitude': 'DOLongitude'}, inplace=True)
df.drop(columns=['LocationID'], inplace=True)
# 병합 후 NaN 값이 있는지 확인
print(df.isnull().sum())
# NaN 값이 있는지 확인하고 제거
df_cleaned = df.dropna(subset=['PULatitude', 'PULongitude', 'DOLatitude', 'DOLongitude'])
from geopy.distance import geodesic
# 지리적 거리를 계산하는 함수 정의 (마일 단위로 계산)
def calculate_distance_miles(pickup_coords, dropoff_coords):
return geodesic(pickup_coords, dropoff_coords).miles # 마일 단위로 계산
# 픽업과 드롭오프 지점의 좌표로부터 거리 계산
df_cleaned['distance_miles'] = df_cleaned.apply(
lambda row: calculate_distance_miles((row['PULatitude'], row['PULongitude']),
(row['DOLatitude'], row['DOLongitude'])), axis=1)
# 가장 멀리 떨어진 두 지점 찾기
max_distance_record = df_cleaned.loc[df_cleaned['distance_miles'].idxmax()]
max_distance = max_distance_record['distance_miles']
print(f"가장 멀리 있는 두 지점 사이의 거리: {max_distance} miles")
# 그 거리 이상인 레코드를 필터링해서 제거
df_filtered = df_cleaned[df_cleaned['distance_miles'] <= max_distance]
print(f"필터링 후 남은 레코드 수: {len(df_filtered)}")
df_filtered.to_csv("df_filtered_distance.csv")
그러나 1시간이 지나도 해당 결과를 보지 못하였고, 반복해서 MemoryError가 발생하였다.
이에 맨해튼 거리방법으로 해당 극단값을 해결해보고자 하였다.
5) 맨해튼 거리
# 맨해튼 거리 계산
df['manhattan_distance'] = abs(df['Latitude_pickup'] - df['Latitude_dropoff']) + abs(df['Longitude_pickup'] - df['Longitude_dropoff'])
# 최대 거리 설정
max_distance = df['manhattan_distance'].max()
# 맨해튼 거리 이상인 레코드를 필터링하여 제거
df_filtered = df[df['manhattan_distance'] <= max_distance]
df = df_filtered.copy()
# PULocationID가 location_df에 없는 경우 확인
pulocation_missing = df[~df['PULocationID'].isin(location_df['LocationID'])]
# DOLocationID가 location_df에 없는 경우 확인
dolocation_missing = df[~df['DOLocationID'].isin(location_df['LocationID'])]
# 결과 출력
print(f"LocationID가 없는 PULocationID 레코드 수: {len(pulocation_missing)}")
print(f"LocationID가 없는 DOLocationID 레코드 수: {len(dolocation_missing)}")
# PULocationID가 location_df에 없는 고유 값 확인
missing_pulocation_ids = pulocation_missing['PULocationID'].unique()
print(f"LocationID가 없는 PULocationID 목록: {missing_pulocation_ids}")
# DOLocationID가 location_df에 없는 고유 값 확인
missing_dolocation_ids = dolocation_missing['DOLocationID'].unique()
print(f"LocationID가 없는 DOLocationID 목록: {missing_dolocation_ids}")
# 제외할 LocationID 목록
exclude_locations = [264, 265, 57, 105]
# PULocationID와 DOLocationID에서 제외하는 필터링 조건 적용
df = df[~df['PULocationID'].isin(exclude_locations) & ~df['DOLocationID'].isin(exclude_locations)]
LocationID를 살펴보고 승차 및 하차 위치 코드에 없는 번호를 찾아 삭제 처리하였다.
C. 분석 모형
- 목적: 택시기사에게 최대 수익을 가져다 줄 수 있는 승객의 탑승 위치를 예측하고자 한다.
- 기본 형식 지정
df = pd.read_csv('yellow_taxi_data_mega_df.csv')
# 컬럼 타입 변경
df['tpep_pickup_datetime'] = pd.to_datetime(df['tpep_pickup_datetime'])
df['tpep_dropoff_datetime'] = pd.to_datetime(df['tpep_dropoff_datetime'])
# 'pickup_day' 열을 새로 생성 (요일 정보 추출)
df['pickup_day'] = df['tpep_pickup_datetime'].dt.dayofweek # 0: 월요일, 6: 일요일
df['hour'] = df['tpep_pickup_datetime'].dt.hour
1. KMeans를 활용한 분석
- 모델 사용 이유: 승차 위치 그룹화를 통해 효율적인 운영 전략을 도출하기 위해 최적의 승차 위차를 찾고자 함.(위치 기반 시스템을 개선하고자 함)
1) KMeans 코드
# 위도와 경도를 이용한 클러스터링
locations = df[['Latitude', 'Longitude']].dropna().drop_duplicates()
# KMeans 클러스터링 적용
kmeans = KMeans(n_clusters=10, random_state=42).fit(locations)
# 클러스터링 대상이 된 데이터프레임의 인덱스 추출 (dropna를 고려한)
valid_indices = df[['Latitude', 'Longitude']].dropna().index
# 각 승차 위치에 클러스터 할당 (dropna된 행에 대해 클러스터 예측)
df.loc[valid_indices, 'cluster'] = kmeans.predict(df.loc[valid_indices, ['Latitude', 'Longitude']])
# 특정 시간대와 요일에 대해 클러스터별 평균 수익 계산
filtered_data = df[(df['hour'] == hour_input) & (df['pickup_day'] == day_input)]
# NaN 값을 제거한 후, 클러스터별 평균 수익을 계산
cluster_revenues = filtered_data.groupby('cluster')['total_amount'].mean().reset_index()
# 수익이 높은 순으로 클러스터 정렬
cluster_recommendations = cluster_revenues.sort_values(by='total_amount', ascending=False)
# 상위 3개의 클러스터 추천
top_clusters = cluster_recommendations.head(3)
print("추천 클러스터:", top_clusters)
# 해당 클러스터의 대표 승차 위치 추천
for cluster in top_clusters['cluster']:
location_in_cluster = df[df['cluster'] == cluster][['PULocationID', 'Latitude', 'Longitude']].drop_duplicates().head(5)
print(f"클러스터 {cluster}의 추천 승차 위치:")
print(location_in_cluster)
# 산점도: 위도와 경도를 기준으로 클러스터에 따라 색을 다르게 표현
sns.scatterplot(
x='Longitude',
y='Latitude',
hue='location_cluster', # 클러스터에 따라 색상을 다르게 설정
data=filtered_data, # 데이터프레임을 random_sample_100k로 수정
palette='viridis',
legend='full'
)
# 클러스터 중심 계산
centroids = filtered_data.groupby('location_cluster')[['Latitude', 'Longitude']].mean().reset_index()
# 클러스터 중심에 마커 추가
plt.scatter(
centroids['Longitude'],
centroids['Latitude'],
s=100, # 마커 크기
c='red', # 마커 색상
label='Centroids', # 범례 이름
marker='X' # 마커 형태 (X 모양으로 표시)
)
# 그래프 설정
plt.title('Cluster Visualization of Taxi Pickups')
plt.xlabel('Longitude')
plt.ylabel('Latitude')
plt.legend(title='Cluster')
# 그래프 출력
plt.show()
1. Cluster 0 (보라색): - 위치: 대부분의 데이터 포인트가 뉴욕의 서쪽에 위치해 있다. 주로 로어 맨해튼 또는 허드슨 강 근처일 가능성이 있다. - 특성: 이 클러스터는 다른 클러스터들보다 좀 더 넓은 범위에 퍼져있으며, 도시 외곽에 더 가깝다.
2. Cluster 1 (파란색): - 위치: 뉴욕 시 맨해튼의 중심부 근처에 집중되어 있다. - 특성: 이 클러스터는 맨해튼 북부 또는 중부 지역의 택시 픽업 위치를 나타낼 수 있다. 도시 내에서 더 밀집된 픽업 패턴을 보여주고 있다.
3. Cluster 2 (녹색): - 위치: 뉴욕 시의 동쪽, 특히 퀸즈나 브루클린으로 추정되는 지역에서 주로 발생하는 픽업을 나타낸다. - 특성: 이 군집은 도시 중심에서 떨어진 곳에서 주로 형성된 클러스터이다.
4. Cluster 3 (노란색): - 위치: 맨해튼 남부와 허드슨 강에 가까운 지역. - 특성: 맨해튼의 하단부에서 매우 밀집된 픽업 위치가 많으며, 뉴욕의 핵심 중심지에 있는 클러스터일 가능성이 크다.
5. 군집 특성 분석: - 지리적 범위: 각 클러스터가 나타내는 위치에 따라 주요 택시 픽업 지역의 밀집도를 보여준다. Cluster 0과 Cluster 2는 도시 외곽에서 픽업되는 빈도가 높은 반면, Cluster 1과 3은 맨해튼의 중심부에서 발생하는 픽업이다. - 클러스터 중심: 각 클러스터의 중심은 군집의 평균적인 픽업 위치를 나타내며, 픽업 활동이 활발한 지역을 확인하는 데 유용하다.
3. Random Forast Regressor 를 활용한 분석
- 모델 사용 이유: 여러 개의 의사결정트리를 사용해 과적합 방지 및 예측 성능을 향상하기 위해 사용함.
- 회귀분석: 운행 거리와 총 수익간의 관계를 분석하고 회귀 직선을 시각화 하기 위해 진행함.
1) 머신러닝 모델 코드
# 모델에 사용할 특성
features = random_sample_100k[['pickup_day_of_week', 'pickup_hour', 'location_cluster', 'manhattan_distance', 'total_amount']]
target = random_sample_100k['RatecodeID']
# 데이터를 학습용과 테스트용으로 분할
X_train, X_test, y_train, y_test = train_test_split(features, target, test_size=0.2, random_state=42)
# 랜덤 포레스트 모델로 학습
rf_model = RandomForestRegressor(n_estimators=100, random_state=42)
rf_model.fit(X_train, y_train)
# 테스트 데이터를 통해 예측
y_pred = rf_model.predict(X_test)
# 모델 성능 평가 (RMSE)
mse = mean_squared_error(y_test, y_pred)
rmse = mse ** 0.5
print(f'평균 제곱근 오차(RMSE): {rmse:.2f}')
# 평균 제곱근 오차(RMSE): 13.81
- Accuracy 0.99인 값이 측정되었다.
2) 실제값과 예측값의 산점도
수치가 작은 값들은 잘 맞지만, 적절한 모델은 아닌 것으로 보인다.
4. LightGBM 사용하여 재분석
# 클러스터링을 위한 위도, 경도 기반 KMeans 적용
kmeans = KMeans(n_clusters=4, random_state=42) # 클러스터 수는 4로 설정
df['location_cluster'] = kmeans.fit_predict(df[['Latitude', 'Longitude']])
# 추가 피처로 수익성 관련 칼럼 선택
X = df[['pickup_day_of_week', 'pickup_hour','pickup_latitude','pickup_longitude','dropoff_latitude','dropoff_longitude']]
y = df['total_amount']
# 데이터 분리 (학습 데이터와 테스트 데이터로 나누기)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
# 훈련 및 테스트 데이터 분할
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
# LightGBM 회귀 모델 정의
lgb_model = lgb.LGBMRegressor(
n_estimators=1000,
learning_rate=0.05,
max_depth=10,
random_state=42,
n_jobs=-1
)
# 모델 학습 - early stopping 및 학습 진행 상황 출력
lgb_model.fit(
X_train, y_train,
eval_set=[(X_test, y_test)],
eval_metric='rmse', # 평가 메트릭 설정
callbacks=[
lgb.early_stopping(stopping_rounds=100), # early stopping 콜백
lgb.log_evaluation(50) # 50번마다 학습 로그 출력
]
)
# 예측
y_pred = lgb_model.predict(X_test)
# 모델 성능 평가 (RMSE)
rmse = mean_squared_error(y_test, y_pred, squared=False)
print(f"RMSE: {rmse}")
# 예측값이 연속형이므로 회귀 평가지표 사용
print("RMSE:", mean_squared_error(y_test, y_pred, squared=False)) # Root Mean Squared Error
print("MAE:", mean_absolute_error(y_test, y_pred)) # Mean Absolute Error
print("R² Score:", r2_score(y_test, y_pred)) # R^2 Score (결정 계수)
# RMSE: 6.15423934688325
# MAE: 3.534872188639418
# R² Score: 0.8479843617006366
nyc_map = folium.Map(location=[40.7128, -74.0060], zoom_start=12)
for index, row in top_locations_with_coords.iterrows():
folium.Marker([row['Latitude'], row['Longitude']],
popup=f"Location ID: {row['LocationID']}").add_to(nyc_map)
- 해당 위치와 클러스터의 중심이 비슷한 것으로 보아 Top 20 곳 혹은 Top10 위치로 범위를 좁혀 진행하였다.
4. 승하차 지역 중 가장 많은 지역 Top10 데이터를 활용한 머신러닝
1) 데이터 준비
# Top10 안에 들어가는 값만 추출
filtered_data = df[(df['PULocationID'].isin(top_locations_with_coords['LocationID'])) |
(df['DOLocationID'].isin(top_locations_with_coords['LocationID']))]
filtered_data.describe()
2) 클러스터링을 위한 위도, 경도 기반KMeans 적용 및 피쳐, 타겟 변수 변경
# 클러스터링을 위한 위도, 경도 기반 KMeans 적용
kmeans = KMeans(n_clusters=4, random_state=42) # 클러스터 수는 4로 설정
df['location_cluster'] = kmeans.fit_predict(df[['Latitude', 'Longitude']])
# 추가 피처로 수익성 관련 칼럼 선택
X = df[['pickup_day_of_week', 'pickup_hour','pickup_latitude','pickup_longitude','dropoff_latitude','dropoff_longitude']]
y = df['total_amount']
# 훈련 및 테스트 데이터 분할
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
# LightGBM 회귀 모델 정의
lgb_model = lgb.LGBMRegressor(
n_estimators=1000,
learning_rate=0.05,
max_depth=10,
random_state=42,
n_jobs=-1
)
# 모델 학습 - early stopping 및 학습 진행 상황 출력
lgb_model.fit(
X_train, y_train,
eval_set=[(X_test, y_test)],
eval_metric='rmse', # 평가 메트릭 설정
callbacks=[
lgb.early_stopping(stopping_rounds=100), # early stopping 콜백
lgb.log_evaluation(50) # 50번마다 학습 로그 출력
]
)
# 예측
y_pred = lgb_model.predict(X_test)
# 모델 성능 평가 (RMSE)
rmse = mean_squared_error(y_test, y_pred, squared=False)
print(f"RMSE: {rmse}")
# 예측값이 연속형이므로 회귀 평가지표 사용
print("RMSE:", mean_squared_error(y_test, y_pred, squared=False)) # Root Mean Squared Error
print("MAE:", mean_absolute_error(y_test, y_pred)) # Mean Absolute Error
print("R² Score:", r2_score(y_test, y_pred)) # R^2 Score (결정 계수)
결과는 아래와 같다.
R 스퀘어 값이 0.8로 지나치게 높아 보인다.
3) 실제값과 예측 값 산점도를 그려본다면 아래와 같은 결과가 나타난다.
- 실제값과 예측값 간의 산점도를 기본적으로 한 번 보고, 로그변환을 하여 보고, 축범위를 제한하여 보았다. 대부분의 그래프에서 낮은 값에 대해 높은 정확도를 보여주고 있다.
D. 마무리
모델의 초기 분석에서는 충분한 인사이트를 도출하지 못했지만, 이를 통해 추가적인 분석 방향을 설정할 수 있었다. 특히 클러스터링을 통해 도출한 추천 승차 위치와 위도·경도 정보를 보면, 이 값들이 프로젝트의 목표 달성에 중요한 역할을 할 수 있었을 것이라는 생각이 든다. 그러나 분석 과정에서 데이터를 충분히 이해하지 못한 채 기계적으로 분석한 결과, 프로젝트를 중도에 마무리하게 된 것 같다. 앞으로는 고객의 하차 지점에서 다음 고객의 목적지를 예측해, 더 높은 수익을 낼 수 있는 고객을 어떻게 태울 수 있을지에 대한 분석을 진행하고자 한다. 택시 운행은 하나의 사이클로 볼 수 있다. 손님을 내려준 후 빈 차로 돌아오는 상황을 줄이지 않으면 수익에 손해가 발생한다. 따라서 빈 차로 도로를 달리는 시간을 최소화하고, 더 높은 수익을 낼 수 있는 고객의 목적지를 예측하는 모델을 구축하고 싶다. 이를 위해 고객의 목적지와 다음 승객의 예상 위치를 함께 고려한 최적 경로 예측 모델을 탐색하여 택시 기사의 운영 효율성을 향상시키는 방안을 마련할 계획이다.
해당 데이터는 전자 상거래 플랫폼의 소매 부문 판매 채널과 관련된 것이다. 가장 정확한 보고서를 작성하기 위해서는 수익과 이익을 지난달 및 전년도 같은 기간과 비교하여 효과적인 마케팅 전략을 수립하는 것이 중요하다. 또한, 이익을 기준으로 상위 성과 제품을 식별하고 이러한 제품에 대한 세부 분석을 진행해야 하며, 미래 개발을 위한 성장 가능성이 있는 제품도 함께 평가해야 한다.
해당 데이터는 orders, order_items, customers, payments, products 의 총 5개 개별 파일로 하나의 데이터로 만들어 사용할 예정이다. cleaned csv 파일이 있지만 어떤 기준으로 어떤 방식으로 정제되었는지 모르기 때문에 사용하지 않는다.
2. 칼럼 확인
orders
- order_id(PK): 주문의 고유 식별자, 이 테이블의 기본 키 역할 - customer_id: 고객의 고유 식별자 - order_status: 주문 상태. 예: 배송됨, 취소됨, 처리 중 등 - order_purchase_timestamp: 고객이 주문을 한 시점의 타임스탬프 - order_approved_at: 판매자가 주문을 승인한 시점의 타임스탬프 - order_delivered_timestamp: 고객 위치에 주문이 배송된 시점의 타임스탬프 - order_estimated_delivery_date: 주문할 때 고객에게 제공된 예상 배송 날짜
order_items
- order_id(PK): 주문의 고유 식별자 - order_item_id(PK): 각 주문 내 항목 번호. 이 컬럼과 함께 order_id가 이 테이블의 기본 키 역할 - product_id: 제품의 고유 식별자 - seller_id: 판매자의 고유 식별자 - price: 제품의 판매 가격 - shipping_charges: 제품의 배송에 관련된 비용
customers
- customer_id(PK): 고객의 고유 식별자, 이 테이블의 기본 키 역할 - customer_zip_code_prefix: 고객의 우편번호 - customer_city: 고객의 도시 - customer_state: 고객의 주
payments
- order_id: 주문의 고유 식별자, 이 테이블에서 이 컬럼은 중복될 수 있다 - payment_sequential: 주어진 주문에 대한 결제 순서 정보를 제공 - payment_type: 결제 유형 예: 신용카드, 직불카드 등 - payment_installments: 신용카드 결제 시 할부 회차 - payment_value: 거래 금액
products
- product_id: 각 제품의 고유 식별자, 이 테이블의 기본 키 역할 - product_category_name: 제품이 속한 카테고리 이름 - product_weight_g: 제품 무게 (그램) - product_length_cm: 제품 길이 (센티미터) - product_height_cm: 제품 높이 (센티미터) - product_width_cm: 제품 너비 (센티미터)
3. 데이터 하나의 파일로 합치기
customers_df = customers_df.drop_duplicates()
# orders_df와 customers_df를 customer_id를 기준으로 병합
merged_df = pd.merge(orders_df, customers_df, on='customer_id', how='left')
# merged_df와 order_items_df를 order_id를 기준으로 병합
merged_df = pd.merge(merged_df, order_items_df, on='order_id', how='left')
# merged_df와 payments_df를 order_id를 기준으로 병합
merged_df = pd.merge(merged_df, payments_df, on='order_id', how='left')
# merged_df와 products_df를 product_id를 기준으로 병합
final_df = pd.merge(merged_df, products_df, on='product_id', how='left')
# 결과를 CSV 파일로 저장
# final_df.to_csv("merged_data_final.csv", index=False)
merge_df = final_df
현재 수치형 변수가 오른쪽으로 꼬리가 긴 right-skewed 분포를 가지고 있다. 이에 로그 변환 방법으로 변수들을 조금 더 정규분포에 가깝게 만들어 비대칭성을 줄이고, 정규화(Nomalization)을 통해 데이터의 범위를 일정한 구간으로 변환하여 모델의 학습속도와 성능을 높이고자 한다.
해당 데이터는 범주형 변수가 많고, 해당 변수의 값들 또한 많기 때문에 One-Hot Encoding 방식은 적절하지 않을 것으로 사료된다. 이에 범주형 변수의 카테고리를 줄인 후에 Label-Encoding를 하고자 한다. 또한 위치 정보를 나타내는 칼럼들은 5개의 큰 구역으로 묶는 절차를 거친다.
# 범주형 변수들 처리 전 날짜 변수들의 dtype datetime으로 변경.
date_columns = ['order_purchase_timestamp', 'order_approved_at',
'order_delivered_timestamp', 'order_estimated_delivery_date']
for col in date_columns:
merged_df_cleaned[col] = pd.to_datetime(merged_df_cleaned[col])
merged_df_cleaned[date_columns].dtypes
def elbow(df):
sse = []
for i in range(1,15):
km = KMeans(n_clusters= i, init='k-means++', random_state=42)
km.fit(df)
sse.append(km.inertia_)
plt.plot(range(1,15), sse, marker = 'o')
plt.xlabel('cluster count')
plt.ylabel('SSE')
plt.show()
elbow(cluster_1)
3. 군집 결과 해석
# 원본 자료는 살리기 위해 복사하기
merged_clu_df = merged_df_cleaned.copy()
# 클러스트 항목 설정
cluster_1 = merged_df_cleaned[['repeat_order','total_price', 'avg_price', 'total_payment_value', 'avg_payment_value']]
# 학습하기
# 군집 3개로 나누기
custoemr_kmeans3 = KMeans(n_clusters=3, init='k-means++', max_iter=300, random_state=42)
custoemr_kmeans3.fit(cluster_1)
# 군집된 결과 저장
merged_clu_df['cluster'] = custoemr_kmeans3.labels_
# 컬럼 지우는 함수
def del_cols(df):
del df['order_id']
del df['customer_id']
del df['seller_id']
del df['product_id']
del df['order_approved_at']
del df['order_delivered_timestamp']
del df['order_estimated_delivery_date']
# 군집 특성 확인하는 함수
def character_visual(df):
plt.figure(figsize=(20,20))
for i in range(len(df.columns)):
cols = list(df.columns)[i]
plt.subplot(4,6,i+1)
sns.histplot(df, x=cols, palette='RdYlGn')
plt.title(cols)
1) 1번 군집
# 1번 군집만 설정
merged_clu1_df = merged_clu_df[merged_clu_df['cluster'] == 0]
del_cols(merged_clu1_df)
character_visual(merged_clu1_df)
graph = df.value_counts(['Age'])
graph.plot(kind= 'bar', figsize=(10,10),stacked = False, color = colors)
# 제목과 레이블 추가
plt.title('Age')
plt.xlabel('Age')
plt.ylabel('Frequency')
plt.xticks(rotation=0, ha='center')
# x축 ticks 설정 (명명하기)
new_labels = ['20-35', '12-20', '35-60'] # 새로운 레이블
plt.xticks(ticks=range(len(new_labels)), labels=new_labels, rotation=0, ha='center')
1) Age, Gender
사용자 연령대
사용자 성별
- 20~35세 사이의 주사용자이며, 여성 비율이 압도적으로 많다.
2) Genre, Time
음악 장르 선호도
음악 감상 시간
- Melody 장르를 압도적으로 선호한다.
- Pop 과 Classical 장르도 상대적으로 높은 빈도를 보였으나 Melody에 비해 현저히 적은 빈도를 나타낸다.
- 주로 밤에 스포티파이를 이용해 음악을 감상한다.
3) About Spotify Subscription
현재 스포티파이 구독 방법
스포티파이 구독 기간
추후 구독 의사
구독료 지불 의사
- 무료 사용자(81.8%)가 무료(광고 지원)버전을 사용하고 있으며, 프리미어 사용자는 18.2%이다.
- '2년 이상' 사용한 사용자 수가 가장 많다.
- 응답자의 64.1%가 프리미엄 구독을 계속하지 않겠다고 응답하였으며, 35.9%가 계속 구독할 의향이 있다 답하였다.
- 프리미엄 구독을 원하지 않는 사용자가 가장 많았고, 그 뒤를 이어 개인 플랜과 학생 플랜이 비슷한 빈도로 선택되었다.
4) User Preference
음악 감상 분위기
음악 감상 시 사용 장치
음악 듣는 상황
추천 음악 만족도
- 'Relaxation and stree relief' 기분일 때 Spotify 이용에 가장 큰 선호도를 보인다. 이 감정일 때 다양한 음악 장르가 인기있다.
- 'Uplifting and motivational' 기분일 때 또한 높은 선호도를 나타낸다.
- '스마트폰'으로 Spotify를 이용하는 사용자가 가장 많다. '컴퓨터 또는 노트북'과 '스마트 스피커 또는 음성 비서'가 그 뒤를 따라온다.
- 여행 중 또는 여가 시간에 음악을 듣는 비율이 가장 높다.
- 음악 추천 점수는 3.0에서 4.0 사이에 집중되어 있다.
4) Information
사용자 연령별 선호 장르
시간대별 감상 장르
상황별 음악 장르 선택
사용자 연령별 시간대별 감상 장르
음악 플레이스트 선정 방식
장르별 음악 플레이스트 선정 방법
구독 유형별 음악 추천 평균 점수
시간대별 음악 감상 장치
- 대부분의 사람들이 음악을 밤에 가장 많이 듣는다.
- 20~35세 연령대에서 선호하는 음악 장르는 다양하다.
- 20~35세 연령대는 밤 시간대에 K-POP 및 Melody 장르에서 가장 많은 수치를 기록하고 있고, 다른 시간대 보다 더 다양한 장르의 음악이 소비한다.
- 다양한 장르가 야간에 청취되는 비율이 높아, 사용자의 음악 취향의 폭이 넓다는 점을 나타난다.
- Kpop장르는 Melody를 통해 새로운 음악을 가장 많이 발견되고 있다.
- Pop장르 역시 Playlist 와 Recommendations 를 통해 많이 발견되고 있다.
- 'Smartphone'을 가장 많이 사용하며, 특히 밤 시간대의 청취량이 높다.
B. 인사이트
음악소비패턴과 사용자 선호도에 대한 5가지 인사이트를 도출해낼 수 있다.
첫째, 연령대와 음악장르 선호에 대한 분석 결과, 20~35세 연령대에서 Kpop과 Melody 장르에 대한 선호도가 특히 두드러졌다. 이들은 다양한 음악장르에서 높은 선호도를 보이며, 주로 밤 시간대에 음악을 소비하는 경향이 있다. 이는 일이나 학업을 마친 후 여가 시간에 음악을 듣고자 하는 욕구와 관련이 있는 것으로 보인다. 반면 12~20세와 35~60세 연령대는 상대적으로 낮은 선호도를 보이며, 특정 장르에 대한 선호가 분산되어 있는 경향이 있다. 마케팅 전략이나 콘텐츠 개발 시 20~35세 사용자를 주요 타겟으로 삼는 것이 중요하다는 것을 시사한다.
둘째, Spotify 사용기기에 대한 분석 결과, 스마트폰이 300회 이상으로 가장 많은 선택을 받았다. 이는 사용자들이 언제 어디서든 쉽게 음악을 청취할 수 있는 환경을 제공하고 있음을 나타낸다. 특히 젊은 세대는 이동 중에도 스마트폰을 통해 음악을 소비하며, Kpop과 트렌딩 송이 가장 인기 있는 자리로 자리 잡고 있다.
셋째, 구독계획에 대한 분석에서는 81.8%의 사용자가 무료(광고 지원) 버전을 사용하고 있으며, 프리미어 사용자는 18.2%에 불과하다는 결과가 나타났다. 이는 광고가 포함된 무료 서비스 대다수가 사용자에게 선호되고 있음을 보여준다. 많은 사용자가 프리미엄 서비스에 대한 필요성을 느끼지 못하고 있으며, 사용자 경험을 개선하고 프리미엄 서비스의 가치를 높이기 윟나 추가적인 조치가 필요하다. 이를 위해 가격, 필요성, 기능 등을 고려한 사용자 의견 조사 등이 추가적으로 진행되어야 하며, 소비자들이 프리미엄 서비스로 전환하도록 유도하는 다양한 프로모션 전략과 혜택 제공이 있어야 한다.
넷째, 사용자는 주로 스트레스 해소와 긴장감 완화를 위해 음악을 듣는 경향이 있으며, 이는 긍정적인 기분을 유도하는 음악 장르를 강조하는 마케팅 전량이 효과적일 수 있음을 시사한다. 편안한 음악이나 명상 음악을 추천하는 켐페인은 사용자들이 더 많은 음악을 소비하게 할 가능성이 높다. 현대 사회에서 스트레스가 증가하는 사회 분위기 속 음악이 사람들에게 힐링의 역할을 할 수 있는 기회를 마련하는 것이 중요하다.
마지막으로, 프리미엄 지속 의향에 대한 분석에 따르면 응답자의 64.1%가 프리미엄 구독을 계속하지 않겠다고 응답하였다. 이는 고객 이탈의 위험 신호로, 추가적인 기능이나 혜택을 제공하여 사용자 만족도를 높이는 것이 중요하다. 독점 콘텐츠 제공, 사용자 맞춤형 추천 시스템 등이 이에 도움이 될 수 있다. 주 소비층이 소셜미디어 사회에 민감한 20~35세라는 것은 감안할 때, 앱 사용자간의 네트워크를 강화하고, 그들을 위한 차별화된 콘덴츠를 제공하는 것이 필요하다. 인기 아티스타의 라이브 공연 재생, 독점 인터뷰 등을 앱 전용으로 제공하는 방법이 있다. 소비자들은 독점 콘텐츠에 높은 가치를 두는 경향성이 있으므로 앱 활용도가 증가할 것이고, 스마트폰앱에서만 제공되는 콘텐츠는 사용자 충성도를 높이는데 기여할 것이다.
또한 챌린지 콘텐츠는 사용자 참여를 유도하고 자연스러운 마케팅 효과를 창출하는데 중요한 역할을 할 수 있다. 유명 인플루언서들이 참여할 경우, 그 효과는 더 거대해질 것이다. 사용자들이 공유한 콘텐츠는 타인에게 영향을 미치고 스포티파이만의 새로운 커뮤니티를 형성할 수 있는 기회를 제공할 것이다. 정적인 챌린지에서부터 노래, 춤 등의 동적인 챌린지까지 다양한 콘텐츠를 제공함으로써, 사용자들은 자신의 음악 청취 순간을 공유하고 더 많은 상호작용을 할 수 있다.
C. 결론
해당 분석은 Spotify의 음악 소비 패턴과 사용자 선호도를 이해하는데 중요한 기초자료가 될 것이다. 특히, 사용자 참여를 중심으로 한 챌린지 콘텐츠는 Spotify가 음악 소비의 새로운 패러다임을 선도하는데 중요한 기여를 할 것으로 기대된다. 젊은 세대의 다양한 음악 장르 선호와 스마트폰 사용의 용이함을 바탕으로, Spotify 사용자 간의 커뮤니티 형성을 촉진하고 자연스러운 마케팅 효과를 누릴 수 있다.
앞으로 지속적인 데이터 분석과 소비자 피드백 반영을 통해 유입 고객과 충성 고객의 비율을 높이고 이들의 이탈 비율이 줄여 나가는 것이 필요하다.
D. 여담
스포티파이는 이미 사용자추천 시스템이 매우 잘되어 있는 플랫폼으로 알고 있다. 한국에서 이들이 제대로 된 성과를 내지못한다는 것은 사용자 추천 시스템만의 문제가 아닐 것이다. 전 세계적으로 보았을 때 Spotify는 세계 1위 음악 플랫폼이다. 그렇다면 한국에서는 왜이리 주춤하고 있는 것일까. 필자는 멜론 등 한국에서 음악 플랫폼이 생성되기 시작할 때 함께 시작하지 못했기 때문이라 생각한다. 음악 플랫폼은 마치 은행과도 같다. 소비자들이 플랫폼을 쉽사리 바꾸지 못하고, 매우 사용하기 불편해질 때야 비로소 사용자가 움직이기 때문이다. 나도 여러번 음악 플랫폼을 바꾸어 보았지만, 내 취향에 맞는 음악들로 플레이리스트를 만들 때까지 꽤 많은 에너지를 쏟아야 했다. 이미 대부분의 음악 플랫폼은 이미 사용자 취향에 맞는 추천 시스템이 그들을 너무 잘 알기 때문에, 새 플랫폼에 내 정보를 주고 기존 사용하던 플랫폼과 비슷한 수준의 서비스를 제공받기 위해서는 줠대적 시간이 필요하다. 이 사이 사용자는 이 시간과 노력을 들여 플랫폼을 옮겨야 하는가에 대한 의문이 들 수 있고, 다시 한 번 플랫폼 변경에 대해 저울질할 것이다.
이때 스포티파이는 한국 유저들을 사로잡기 위한 매력적인 포인트가 아쉽게도 불충분하다. 멜론과 같이 아티스트들과 독점 컨텐츠, 콘서트 표 매매 등의 콘텐츠들이 있는 것도 아니며, youtube music 과 같이 프리미엄 구독을 하면 광고로 끊김 없이 영상과 음악 모두 즐길 수 있다는 유혹적인 제안도 없다.
스포티파이가 아무리 사용자 추천 시스템이 잘 되어 있다고 하더라도 사용자가 느끼기에 국내 기존의 플랫폼에서도 거의 비슷한 수준의 서비스를 제공하고 있다면 스포티파이는 한국에서 살아남기 위한 새로운 모험을 떠나야 할 것으로 보인다.
music_Influencial_mood Relaxation and stress relief 195 Uplifting and motivational 67 Sadness or melancholy 55 Relaxation and stress relief, Uplifting and motivational 44 Relaxation and stress relief, Uplifting and motivational, Sadness or melancholy, Social gatherings or parties 35 Relaxation and stress relief, Sadness or melancholy 33 Relaxation and stress relief, Uplifting and motivational, Sadness or melancholy 22 Social gatherings or parties 16 Relaxation and stress relief, Uplifting and motivational, Social gatherings or parties 14 Relaxation and stress relief, Social gatherings or parties 13 Uplifting and motivational, Sadness or melancholy 12 Relaxation and stress relief, Sadness or melancholy, Social gatherings or parties 8 Uplifting and motivational, Social gatherings or parties 4 Sadness or melancholy, Social gatherings or parties 1 Uplifting and motivational, Sadness or melancholy, Social gatherings or parties 1 Name: count, dtype: int64
music_lis_frequency While Traveling 111 leisure time 87 While Traveling, leisure time 65 While Traveling, Workout session, leisure time 48 Workout session 33 Study Hours 19 Office hours 16 While Traveling, Workout session 16 Office hours, While Traveling 12 Office hours, While Traveling, leisure time 12 Office hours, While Traveling, Workout session 10 Study Hours, While Traveling, leisure time 10 Study Hours, While Traveling 9 Office hours, Study Hours, While Traveling, Workout session, leisure time 7 Study Hours, While Traveling, Workout session 7 Office hours, Study Hours, While Traveling 7 Study Hours, Workout session 6 Office hours, leisure time 6 Workout session, leisure time 6 Study Hours, While Traveling, Workout session, leisure time 4 Study Hours, Workout session, leisure time 4 Study Hours, leisure time 4 Office hours, While Traveling, Workout session, leisure time 3 Office hours, Study Hours, While Traveling, leisure time 3 Office hours, Workout session 3 Office hours, While Traveling, 2 Office hours, Study Hours, Workout session 2 While Traveling, Workout session, leisure time, Night time, when cooking 1 Social gatherings 1 Random 1 Office hours, Workout session, leisure time 1 While Traveling, Before bed 1 Office hours, Study Hours, While Traveling, Workout session 1 Office hours, Study Hours, While Traveling, Workout session, leisure time, 1 Office hours,Study Hours, While Traveling, leisure time 1 Name: count, dtype: int64
music_expl_method recommendations 113 Playlists 112 recommendations, Playlists 86 Others 55 Radio 51 Playlists, Radio 18 recommendations, Playlists, Others 18 recommendations, Others 15 recommendations, Playlists, Radio 13 Playlists, Others 9 Radio, Others 7 recommendations, Radio 6 Playlists, Radio, Others 6 recommendations, Radio, Others 4 recommendations, Playlists, Radio, Others 2 recommendations,Others, Social media 1 Others, Social media 1 Others, Friends 1 recommendations, Others, Social media 1 Others, Search 1 Name: count, dtype: int64