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을 활용한 식중독 주요 요인 그래프, 식중독 추이 및 예측값 비교 선그래프 등을 사용하였으며 간결하고 명확한 정보 제공에 주안점을 두었다.

대시보드 예시 페이지

 

+ 함께 프로젝트를 진행한 이동훈님, 이건님, 전도운님, 최영석님께 감사의 인사를 드립니다. 함께할 수 있어 영광이었습니다.

 

A. 미래를 여는 빅데이터와 AI; 더 나은 서울

 2024년 10월 11일, 코엑스에서 열린 2024 서울 빅데이터 포럼이 열렸다. AI와 빅데이터가 도시 문제 해결과 시민의 삶에 미치는 영향에 대한 논의가 이루어졌다. 해당 포럼의 목표는 서울 시민의 삶을 개선하고, 기술적인 혁신을 통한 정책 수립을 지원하는 것으로, 서울의 일상생활, 안전, 미래 도시 발전에 미치는 빅데이터와 AI의 영향력에 대한 토의하였다.

 기조세션에서는 TU Delft의 Yanan Xin 교수와 NAVER 클라우드의 하정우, Microsoft의 Steve Shirkey가 AI와 빅데이터의 도시문제 해결, 안전 강화, 시민의 삶 개선 등에 대한 내용을 발표하였다. 이후 「데이터로 연결된 일상: 쾌적하고 편리한 서울 시민생활」,  「안전한 도시 서울: AI·빅데이터로 강화하는 도시 안전망」,  「데이터 · AI가 그리는 미래도시 서울」이라는  큰 주제로 나누어 발표와 토론이 이어졌다.

 첫 세션, 「데이터로 연결된 일상: 쾌적하고 편리한 서울 시민생활」 에서는 서울의 대기질, 교통 데이터 분석 등을 활용하여 도출한 결과를 토대로 일상생활에서의 데이터 기반 편의성을 높이기 위한 방법들에 대한 논의가 오갔다.

 두 번째 세션, 「데이터 · AI가 그리는 미래도시 서울」에서는 디지털 성범죄 분석 시스템, 건강 빅데이터 기반 치매 관리 등 다양한 사례연구를 소개하였다.

 세 번째 세션, 「데이터 · AI가 그리는 미래도시 서울」은 데이터와 AI를 통한 서울의 미래 발전과 관련하여 통계 분야에서의 AI 도입 사례와 현재 주어진 도전 과제 등에 대해 논의하였다.

B. 발표 회고

 가장 기억에 남는 발표는 기조세션의 하정우 센터장의 "소버린 AI를 넘어 더 나은 시민들의 삶을 위한 포용적 AI" 와 변형균 대표이사의 "AI로 진화하는 도시: 미래 도시의 설계" 이다. 

 최근 ChatGPT의 발전 속도는 가히 경이로울 정도다. 특히, 2023년에 출시된 GPT-4는 기존의 모델들에 비해 더 정교한 대화가 가능하고, 복잡한 문제 해결이 가능해졌다. 이제 곧 있으면 '아이언맨' 영화에 나오는 토니 스타크의 AI 비서 '자비스'가 탄생할 것 같다.

 

1. 소버린 AI를 넘어 더 나은 시민들의 삶을 위한 포용적 AI

 하정우 센터장은 2024년 생성형 AI의 최신 트렌드를 소개하며, 멀티모달 AI, 온디바이스 AI, 오픈소스 기술이 시민들의 일상을 어떻게 변화시킬 것인지에 대하여 발표하였다. 기술의 발전은 AI 시스템의 접근성을 높이고 사용자 친화적으로 만들었다. 그러나 글로벌 AI 환경은 점점 더 치열해지는 경쟁 속에 있으며, 자국의 문화적 가치에 맞는 AI 기술을 개발하고자 노력하고 있다고 한다. 이는 '소버린 AI' 로 이어진다. 네이버 CLOVA에 따른 소버린 AI(Sovereign AI) 란, 국가 또는 특정 정부가 독립적으로 개발하고 관리하는 인공지능 시스템을 지칭하는 개념이다. 특히, 데이터 주권(Data Sovereignty)과 국가 안보 측면에서 중요하게 다뤄지고 있으며, 외부 의존성을 최소화하고 국가의 전략적 이익을 보호하며 민감한 정보와 데이터의 통제권을 확보하는 것이 핵심 목표이다. 다시 말해 데이터를 국가내에서 보호하고 관리하는 것으로, 국가 또는 정부가 자국민의 데이터와 AI 시스템에 대한 통제권을 가지려는 의도를 가졌다고 볼 수 있다. 이와 더불어 네이버가 소버린 AI를 개발하며 적용사례를 소개하였다. 그와 동시에 특정국가를 위한 AI에 그치지 않고, 글로벌 문화 다양성과 포용성을 촉진하는데 중요한 개념으로 모든 국가가 협력하여 모든 시민에게 유익한 AI 기술을 개발할 수 있음을 강조하며 발표가 끝이 났다.

 또한, 네이버의 AI 기술의 발전과 함께 이를 뒷받침하기 위한 데이터 센터 확장 계획을 발표하며. 대규모 데이터를 안정적으로 처리할 수 있는 인프라의 중요성에 대해서도 이야기하였다. AI 기술의 발전에 따라 빠르고 안정적인 데이터 처리 인프라는 필수적이다. 그러나 데이터 센터의 확장에는 단순한 기술적 도약뿐만 아니라 사회적 책임도 함께 포함해야한다 생각한다. 한국토지신탁에서 데이터 센터 개발 사업을 지켜보며 알게 된 사실은 엄청난 전력을 소모한다는 것이다.(전력량을 수치로 보며 받았던 충격을 잊을 수 없다.) 이로 인해 온실가스 배출과 같은 환경 문제를 야기할 수 있다. 그렇기에 기업의 이익뿐 아니라 데이터 센터 건립으로 발생하는 사회적 비용에 대하여 함께 고민해야 한다. 지속가능한 도시, 인프라를 목표로 삼고 있다면, 정부는 이를 뒷받침할 수 있는 정책적 방향성을 제시해야 한다고 본다. AI가 진정 도시문제를 해결할 수 있다면, 정부는 방향을 제시하고 기업은 책임을 다하며, 시민은 참여와 감시를 통해 더 나은 도시를 만들도록 협력해야 한다.

 

2. AI로 진화하는 도시: 미래 도시의 설계

 변형균 대표이사는 AI가 도시의 교통, 환경, 자원 관리 등의 도시 문제 해결에 핵심적인 역할을 한다고 이야기하였다. AI를 통해 교통 혼잡 문제를 해결하고 에너지 사용을 최적화하는 시스템의 필요성을 강조하며, 글로벌 스마트 시티의 사례를 소개하였다. 흔히 AI와 스마트시티에 대한 내용임에도 불구하고 이 발표가 잊혀지지 않는 이유는 현재 진행 상황과 우리가 꿈꾸는 도시의 그 간극에 대해 이야기하고 있기 때문이다.  스마트 교통 시스템과 예측 인프라 유지관리와 같은 AI의 초기 혁신단계를 목격하고 있다. 그러나 기술발전과 사회적 적응의 차이로 인해 AI 기반 도시 유토피아의 완전한 실현에는 여전히 시간이 필요함을 설명한다. 새로운 기술이 더 효율적이고 지속 가능한 도시를 만들기 위한 토대를 마련하고 있지만, 오늘날의 스마트 시티와 미래의 AI 통합 대도시 사이에는 여전히 해결해야하는 문제들이 남아있다. AI기술이 도시 인프라의 효율성을 극대화하고, 궁극적으로 지속 가능한 도시를 만드는데 기여할 것이라는 그의 말이 현실적으로 들렸다. 변형균 대표이사는 앞으로의 6개월 동안의 AI 발전이 이전 6개월의 변화와는 다를 것이라 말하며, AI의 발전이 점진적이지만 근본적으로 도시를 재편하고, 인간의 삶을 향상시키는 방향으로 나아갈 것이라고 전망하였다.

C. 마무리

 이번 포럼은 서울이 빅데이터와 AI를 활용하여 시민들의 삶의 질을 개선할 수 있는 방안에 대해 논의하고, 정책, 거버넌스, 기술 혁신의 중요성을 강조하며 공공 및 민간 부문이 함께 협력할 수 있는 장을 마련하였다. 나에게도 도시 속에 AI가 어떤 방식으로 활용되고 있는지 알 수 있는 좋은 기회였다. 데이터 분석을 기반으로 진행된 다양한 연구사례들을 보면서 현재 공부 중인 데이터 분석이 실제 어떻게 쓰이고 있는지, 결과를 살펴보며 어떤 생각, 통찰을 통해 결론을 낼 수 있는지에 대해 배울 수 있었다.

 우리 도시의 미래를 낙관적으로 바라보는 관점이 인상적이었다. 문제를 분석하고 이를 해결하고자 노력하는 이들이 많아 안심이 되었다. 정책과 기술의 발전 간의 조화가 얼마나 중요한지에 대해 생각해볼 수 있는 귀한 시간이었다.

 

포럼 참석 인증샷

A. 시계열 데이터(Time Series Data)

1. 정의

시간에 따라 정렬된 데이터로, 주기적으로 기록되어 이를 동일한 시간단계(timestep)로 분포한다고 정의한다.

시계열은 추세, 계절성, 잔차로 분해된다.

 

2. 추세(Trend)

지속적이고 일관된 방향으로 변화하는 장기적 패턴을 의미한다.

시계열에서 느리게 움직이는 큰 변화를 나타낸다. 시간이 지남에 따라 시계열이 점진적으로 증가하거나 감소하는 현상을 말한다.

 

3. 계절성(Seasonality)

일정한 시간 주기에 따라 반복되는 패턴으로, 일반적으로 시간에 관련된 고정된 주기를 가지고 있으며 특정시점에서 데이터가 규칙적으로 상승하거나 하락하는 현상을 말한다. 사계절을 의미하는 것이 아니라 순환하는 현상을 이야기한다.

 

4. 주기성(Cycles)

경제적, 사회적, 정치적 요인에 의해 발생하는 불규칙한 변동 패턴으로 장기적으고 불규칙한 패턴이다.

계절적 구성요소는 시계열의 계절적 패턴, 주기는 일정 기간 동안 반복적으로 발생한다.

개념 계절성 주기
규칙성 일정한 간격으로 반복 불규칙적으로 발생
발생 원인 계절적 요인(기후 등) 경제, 사회적인 요인(경기순환)
시간 범위 1년 이내 수년 이상

 

5. 잔차(Residual)

시계열 데이터에서 추세와 계절성을 제거한 후 남는 무작위적 변화량을 의미한다. 특정 패턴을 가지지 않으며 주로 백색소음(White Noise)처럼 랜덤한 요소이다.

 

6. 시계열 예측 vs 회귀 예측

  머신러닝 모델 시계열 모델
종속변수(Y) 수치형, 범주형 주로 수치형
독립변수(X) 다중 변수 가능 단일 변수(주로 시간)
모델 유형 선형회귀, 의사결정나무, 신경망 등 ARIMA, Prophet 등
장점 여러 변수간의 관계를 분석 가능
비선형 패턴 학습 기능
시간의 흐름 포착 기능
이해와 해석이 용이
단점 시간에 따른 의존성 무시(추세, 계절성)
과적합 위험
다변량 분석 제한
비선형 모델링 제한
https://snowgot.tistory.com/120

 

B. 확률 보행(Random Walk) 프로세스

1. 확률보행 정의

평균과 분산이 시간이 지나도 일정하지 않은 비정상 시계열로, 시간이 지남에 따라 무작위로 이동하는 경로를 설명하는 수학적 모델이다. 공식은 아래와 같다.

현재 값은  과거 값, 상수, 백색소음의 입력값으로 결정된 함수이며, 현재 값이 과거 정보와 랜덤한 변동의 영향을 받는다.

 

C. 정상성(Stationarity)

1. 정상성의 정의

시계열의 통계적 성질이 시간에 따라 변하지 않는 상태를 의미한다. 시계열 데이터에서 정상성을 가정하거나 내포하면 모델링과 예측 가능성이 좋아진다. 통계적 성질이란 데이터의 중앙값, 분포, 변동성, 상관성 등의 특성을 나타내며 데이터를 분석하는데 있어 중요한 역할을 한다. 

Nelson-Plosser의 연 평균 물가상승률 데이터를 통해 알아보고자 한다.

# 연도별 평균 물가 상승률 계산
import statsmodels.api as sm
import pandas as pd
import matplotlib.pyplot as plt
data = sm.datasets.macrodata.load_pandas().data
inflation_rate_df = pd.DataFrame({
    'Year': data['year'],
    'Inflation Rate': data['infl']
})
inflation_rate_yearly_avg = inflation_rate_df.groupby('Year').mean().reset_index()

# 데이터프레임 출력
display(inflation_rate_yearly_avg.loc[:5,:])

# 연도별 평균 물가 상승률 시각화
plt.figure(figsize=(10, 5))
plt.plot(inflation_rate_yearly_avg['Year'], inflation_rate_yearly_avg['Inflation Rate'], label='Average Inflation Rate')
plt.title('Average Inflation Rate per Year')
plt.xlabel('Year')
plt.ylabel('Inflation Rate')
plt.grid(True)
plt.show()

2. 정상성 시계열 조건

1) 시계열의 평균이 시간에 따라 변하지 않고 일정함

y_t의 기댓값이 시간t에 관계 없이 상수 μ로 일정함

 

시간이 변해도 데이터가 일정한 평균 주위를 중심으로 변동한다고 볼 수 있다.

2) 시계열의 분산이 시간에 따라 일정함

모든 시점에서 동일한 상수 분산을 나타냄

시간에 관계없이 일정한 변동성을 갖는다.

3) 시계열의 두 시점간 공분산이 시간(t)에 의존하지 않고 시간의 차(h)에만 의존함

시점 t와 t+h 사이의 공분산이 시차 h에만 의존하고, 특정 시점 t에는 의존하지 않음

공분산은 두 확률 변수의 선형 관계를 나타내는 값이며, 시점 t의 데이터와 t+h 시점의 데이터 사이의 공분산이 시간 t에 무관하게 일정하다는 의미를 갖는다.

 

3. 정상성 확보를 위한 데이터 변환 기법

1) 위의 데이터는 평균, 분산, 공분산이 모두 변동하고 있으며, 이는 비정상성을 띈다고 볼 수 있다.

2) 평균 안정화

- 차분(Diffencing)

추세, 계절성을 제거하기 위한 방법으로, 특정 시점과 그 시점 사이 발생하는 변화를 계산한다.

- 이동 평균 평활화(Moving Average Smoothing)

단기적인 변동성을 제거하고 전체적인 추세를 더 명확히 볼 수 있다.

3) 분산 안정화

- 로그 변환

분산을 줄이기 위한 변환으로, 데이터의 스케일이 너무 커지거나 변동성이 큰 경우 유용하다.

- 제곱근 변환

로그효과와 유사하게 분산을 줄이는 효과가 있으며 데이터가 극단적으로 변동하는 경우 유용하다.

 

D. 마무리

계속해서 비정상성을 나타낸다.

 

E. 20240926 내용 추가

정상성 확보를 위한 데이터 변환 기법을 사용하였음에도 불구하고 비정상성을 나타내는 것 같아 기존 그래프와 비교하였다.

1. 차분 적용

주황색 선을 보면 파란 선보다 일정하다.

 

2. 로그 변환

초록색 선이 파란 선보다 일정하다.

3. 제곱근 변환

보라색 선이 파란색 선보다 일정하다.

 

각 방법을 사용하였을 때 확실히 이 전보다 정상성을 갖는 것을 확인할 수 있다. 

전에 그린 그래프의 Y축 값이 일정하지 않았기 때문에 그래프가 계속 널뛰는 것처럼 보였던 것이었다. 그래프를 그리면 축 확인하자.

해당 데이터는 분산 안정화를 시켰을 때 정상성을 갖는 것으로 보인다. 그 이유가 궁금해진다.

A. 금일 학습내용

1. 메모리란 무엇인가

프로젝트를 하면서 가장 많이 본 에러문구는 Out Of Memory(OOM) 이다. 긴 시간 셀을 실행시켜 나온 메모리에러를 볼 때마다 절망적이었다. 그러나 빅데이터를 다루는 사람이라면 메모리 관리도 하나의 능력이다.

 

1) 메모리의 정의

'메모리(Memory) = 기억장치' 이다. 크게 RAM 과 Disk(SSD)로 나눌 수 있고, Disk(SSD)는 우리가 익히 잘 아는 저장공간을 의미한다. 장기적으로 데이터를 보관하는 공간으로 전원이 꺼져도 데이터는 사라지지 않는다.

 

2) RAM의 정의

'Random Access Memory' 로 사용자가 자유롭게 읽고 쓰고 지울 수 있는 기억장치를 의미한다. '주 기억장치'로 분류되며, '책상', '도마' 등으로 비유된다. 램이 많으면 한 번에 많은 일을 할 수 있으며, 이는 책상이 넓으면 그 위에 여러가지 물품을 올려놓을 수 있고, 그 물품을 다시 회수하면 물품이 있었다는 기록은 사라지기 때문이다. 

램은 성능이 좋을수록, 용량이 클수록 더 많은 일을 빠르게 수행할 수 있기 때문에 다다익램이라고도 한다.

 

2. 데이터가 메모리에 저장되는 방법

메모리에는 수 많은 비트(0 혹은 1)가 저장되고 각각의 비트는 고유한 식별자(Unique identifier)를 통해 위치를 사용할 수 있다. 

1) 정수(Integers)

컴퓨터에서는 이진법으로 표현하며, 얼마나 많은 메모리를 할당할 것인지를 말한다. 더 많은 메모리를 할당할 수록, 더 큰 숫자를 담을 수 있다.

  • int8:-128 ~ 127
  • int16: -32768 ~ 32727
  • Int32: -2,147,483,648 ~ 2,147,483,647
  • Int64: -9,223,372,036,854,775,808 ~ 9,223,372,036,854,775,807

부호를 위해 메모리의 단위(1비트)를 사용하지만, 담을 수 있는 숫자를 표현하자면 위와 같다.

 

- 오버플로(Overflow) : 크기에 맞지 않는 데이터를 넣어 의도하지 않은 결과가 나오는 것

파이썬의 데이터타입은 기본적으로 동적(Dynamic)하기 때문에 자동으로 데이터 형이 바뀐다. 이에 큰 숫자를 다루는 것에 큰 주의를 기울여야한다.

아래와 같이 메모리 데이터 타입을 지정하는 방법이 있다. 

int32_cols = ["review_likes"]
int8_cols = ["review_rating"]
cate_cols = ["app_name", "author_app_version"]

memory_usage_before = df.memory_usage().sum()

df.info()
for col in int32_cols:
    assert abs(df[col].max()) < 2_147_483_647 
    df[col] = df[col].astype(pd.Int32Dtype())
    
df.info()
for col in int8_cols:
    assert abs(df[col].max()) < 127
    df[col] = df[col].astype(pd.Int8Dtype())
    
df.info()
for col in cate_cols:
    assert df[col].nunique() < 10_000
    df[col] = df[col].astype("category")
    
df.info()

memory_usage_after = df.memory_usage().sum()
reduction_ratio = 1 - (memory_usage_after / memory_usage_before)

print(f"Memory Usage: {memory_usage_before:,} -> {memory_usage_after:,} ({reduction_ratio*100:.2f}% reduced)")
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 28154385 entries, 0 to 28154384
Data columns (total 8 columns):
 #   Column              Dtype  
---  ------              -----  
 0   review_id           object 
 1   app_name            object 
 2   author_name         object 
 3   pseudo_author_id    object 
 4   author_app_version  object 
 5   review_rating       float64
 6   review_likes        int64  
 7   review_timestamp    object 
dtypes: float64(1), int64(1), object(6)
memory usage: 1.7+ GB
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 28154385 entries, 0 to 28154384
Data columns (total 8 columns):
 #   Column              Dtype  
---  ------              -----  
 0   review_id           object 
 1   app_name            object 
 2   author_name         object 
 3   pseudo_author_id    object 
 4   author_app_version  object 
 5   review_rating       float64
 6   review_likes        Int32  
 7   review_timestamp    object 
dtypes: Int32(1), float64(1), object(6)
memory usage: 1.6+ GB
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 28154385 entries, 0 to 28154384
Data columns (total 8 columns):
 #   Column              Dtype 
---  ------              ----- 
 0   review_id           object
 1   app_name            object
 2   author_name         object
 3   pseudo_author_id    object
 4   author_app_version  object
 5   review_rating       Int8  
 6   review_likes        Int32 
 7   review_timestamp    object
dtypes: Int32(1), Int8(1), object(6)
memory usage: 1.4+ GB
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 28154385 entries, 0 to 28154384
Data columns (total 8 columns):
 #   Column              Dtype   
---  ------              -----   
 0   review_id           object  
 1   app_name            category
 2   author_name         object  
 3   pseudo_author_id    object  
 4   author_app_version  category
 5   review_rating       Int8    
 6   review_likes        Int32   
 7   review_timestamp    object  
dtypes: Int32(1), Int8(1), category(2), object(4)
memory usage: 1.1+ GB
Memory Usage: 1,801,880,772 -> 1,182,813,738 (34.36% reduced)

 

데이터에 맞는 데이터 타입을 부여하였을 때 30%가 넘는 메모리 감소를 확인할 수 있다. 

 

2) 그 외 타입

정수가 아닌 타입으로는 문자열(String, Category), Datetime 등의 데이터 타입이 존재한다. 이는 정수와 마찬가지로 알맞은 메모리를 할당을 통해 메모리 최적화를 유지할 수 있다.

 

B. 마무리

데이터를 다룰 때는 반드시 메모리 최적화를 생각해야겠다. 시간도, 비용도 절감할 수 있는 좋은 방법이 있는데 그걸 하지 아니할 이유가 어디 있겠는가.

A. 목적

택시기사들에게 전달할 파일로, 코로나와 같은 팬데믹 상황이 왔을 때 대비할 수 있게 정보를 전달하고자 한다
상세: 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 데이터 활용

nyc_taxi_zones.json
3.63MB
taxi_zones.csv
3.58MB

 

 

 

3) 2차 전처리 코드

import pandas as pd
from shapely import wkt
from shapely.geometry import MultiPolygon

# 데이터 읽기
map_ny2 = pd.read_csv('taxi_zones.csv')

# MULTIPOLYGON에서 경도와 위도를 추출하는 함수
def extract_centroid(geom):
    try:
        # MULTIPOLYGON 형식의 문자열을 Shapely 객체로 변환
        polygon = wkt.loads(geom)
        
        # 다각형의 중심점(centroid) 계산
        centroid = polygon.centroid
        return centroid.y, centroid.x  # 위도, 경도 반환
    except Exception as e:
        return None, None  # 에러 시 None 반환

# 위도와 경도를 추출하여 새로운 열에 추가
map_ny2['Latitude'], map_ny2['Longitude'] = zip(*map_ny2['the_geom'].apply(extract_centroid))

# 경도와 위도가 있는 행만 필터링
map_ny2 = map_ny2.dropna(subset=['Latitude', 'Longitude'])

# 결과 CSV 저장
map_ny2[['LocationID', 'Latitude', 'Longitude']].to_csv('taxi_zones_latlong.csv', index=False)

print("변환이 완료되었습니다. 결과는 taxi_zones_latlong.csv에 저장되었습니다.")

taxi_zones_latlong.csv
0.01MB

 

 

 

4) 지리적 극단값 처리 코드

기존 데이터와 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()

 

그러나 해당 방법으로 값을 제거하여도 여전히 극단값이 존재하였다.

이에 10만 마일 이상인 값을 제거하는 방법으로 진행하였다.

10만 마일 미만의 값 중 최대값, 최소값

6) total amount 극단값 처리

해당 total amount 열의 극단값은 가능할 수 있는 값이라 보고 남겨두기로 했다.

이제와서 생각해보면 이 값도 삭제했어야 했다.

 

7) 택시 구역의 위도, 경도가 포함되지 않은 LocationID를 파악한 후 삭제 처리

# 택시 구역의 위도, 경도 및 LocationID가 포함된 CSV 파일 로드
taxi_zones_df = pd.read_csv('taxi_zones_latlong.csv')

# taxi_zones_df의 Latitude, Longitude 복제
taxi_zones_df['Latitude_copy'] = taxi_zones_df['Latitude']
taxi_zones_df['Longitude_copy'] = taxi_zones_df['Longitude']

# pickup location
df = df.merge(
    taxi_zones_df[['LocationID', 'Latitude_copy', 'Longitude_copy']]
    .rename(columns={'Latitude_copy': 'pickup_latitude', 'Longitude_copy': 'pickup_longitude'}), 
    how='left', 
    left_on='PULocationID', 
    right_on='LocationID'
)

# dropoff location
df = df.merge(
    taxi_zones_df[['LocationID', 'Latitude_copy', 'Longitude_copy']]
    .rename(columns={'Latitude_copy': 'dropoff_latitude', 'Longitude_copy': 'dropoff_longitude'}), 
    how='left', 
    left_on='DOLocationID', 
    right_on='LocationID'
)

df = df.merge(
    taxi_zones_df[['LocationID', 'Latitude', 'Longitude']], 
    how='left', 
    left_on='DOLocationID', 
    right_on='LocationID'
)

df = df.drop(columns=['LocationID_x', 'LocationID_y'])
print(df.columns)

 

결측치 확인

8) 결측치의 LocationID 확인

# 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)
추천 클러스터:    cluster  total_amount
2      2.0     80.937500
7      7.0     56.521240
9      9.0     50.388571
클러스터 2.0의 추천 승차 위치:
        PULocationID   Latitude  Longitude
23055              1  40.691831 -74.174000
33390              5  40.552659 -74.188484
65674             44  40.525495 -74.233534
65675             23  40.606448 -74.170887
224371           118  40.586555 -74.132979
클러스터 7.0의 추천 승차 위치:
      PULocationID   Latitude  Longitude
13             132  40.646985 -73.786533
1488            10  40.678953 -73.790986
3006           180  40.675595 -73.847043
6016           216  40.676154 -73.819460
7382           219  40.662185 -73.764506
클러스터 9.0의 추천 승차 위치:
        PULocationID   Latitude  Longitude
18423              6  40.600324 -74.071771
19967             11  40.604273 -74.007488
30813             14  40.624835 -74.029892
49596            221  40.618769 -74.073704
103323            67  40.619619 -74.013801

상위 5개 승차 위치의 위도와 경도를 산점도로 시각화, 수익에 따라 점 크기 조절

 

2) 클러스터링 시각화 코드 및 결과

# 클러스터링 결과 시각화
plt.figure(figsize=(10, 8))

# 클러스터별로 색상 구분하여 시각화
plt.scatter(df['Longitude'], df['Latitude'], c=df['cluster'], cmap='viridis', alpha=0.5)

# 클러스터의 중심점도 시각화
centers = kmeans.cluster_centers_
plt.scatter(centers[:, 1], centers[:, 0], c='red', s=200, alpha=0.75, marker='x')

plt.title('Taxi Pickup Location Clusters')
plt.xlabel('Longitude')
plt.ylabel('Latitude')
plt.show()

2. Elbow Method 를 활용한 최적의 클러스터 수 및 군집 분석

# 산점도: 위도와 경도를 기준으로 클러스터에 따라 색을 다르게 표현
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

5. 승하차 지역 중 가장 많은 지역 Top20

1) Top 20 코드

DOtemp = df['DOLocationID'].value_counts().head(20).reset_index()
PUtemp = df['PULocationID'].value_counts().head(20).reset_index()
DOtemp = DOtemp.rename(columns={'DOLocationID': 'LocationID'})
PUtemp = PUtemp.rename(columns={'PULocationID': 'LocationID'})
data = pd.merge(DOtemp['LocationID'],PUtemp['LocationID'],on='LocationID', how='outer')

 

2) Top 20 시각화

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. 마무리

 모델의 초기 분석에서는 충분한 인사이트를 도출하지 못했지만, 이를 통해 추가적인 분석 방향을 설정할 수 있었다. 특히 클러스터링을 통해 도출한 추천 승차 위치와 위도·경도 정보를 보면, 이 값들이 프로젝트의 목표 달성에 중요한 역할을 할 수 있었을 것이라는 생각이 든다. 그러나 분석 과정에서 데이터를 충분히 이해하지 못한 채 기계적으로 분석한 결과, 프로젝트를 중도에 마무리하게 된 것 같다.
 앞으로는 고객의 하차 지점에서 다음 고객의 목적지를 예측해, 더 높은 수익을 낼 수 있는 고객을 어떻게 태울 수 있을지에 대한 분석을 진행하고자 한다. 택시 운행은 하나의 사이클로 볼 수 있다. 손님을 내려준 후 빈 차로 돌아오는 상황을 줄이지 않으면 수익에 손해가 발생한다. 따라서 빈 차로 도로를 달리는 시간을 최소화하고, 더 높은 수익을 낼 수 있는 고객의 목적지를 예측하는 모델을 구축하고 싶다.
 이를 위해 고객의 목적지와 다음 승객의 예상 위치를 함께 고려한 최적 경로 예측 모델을 탐색하여 택시 기사의 운영 효율성을 향상시키는 방안을 마련할 계획이다.

+ Recent posts