데이콘 프로젝트 - 농산물 가격예측 1-2 데이터 전처리
데이터분석가로 공부를 하면서, 데이터에 흥미를 갖기위해 관심있는 수강생들과함께 데이콘에 참여해보기로 했습니다. 처음으로 대회를 참여해보는데요, 흥미와 경험목적으로 참여하는만큼, 부담없이 최대한 즐기는 느낌으로 프로젝트를 진행하고자 합니다. 저희가 참여하는 대회는
국민생활과 밀접한 10가지 농산물 품목의 가격을 예측
해야하는데요, 첫번째로 대회에서 제공하는 데이터들을 살펴보는 시간을 가졌고, 데이터 전처리 정리과정을 기록으로 남기고자합니다.
데이터 전처리
1
2
3
4
5
6
7
8
9
10
11
import pandas as pd
# train 폴더 파일 읽기
train_file_path = '/content/drive/MyDrive/데이콘/농산물/train/train.csv'
train = pd.read_csv(train_file_path)
meta_sanji_file_path = '/content/drive/MyDrive/데이콘/농산물/train/meta/TRAIN_산지공판장_2018-2021.csv'
train_sanji = pd.read_csv(meta_sanji_file_path)
meta_jeonguk_file_path = '/content/drive/MyDrive/데이콘/농산물/train/meta/TRAIN_전국도매_2018-2021.csv'
train_jeon = pd.read_csv(meta_jeonguk_file_path)
1
train.head()
시점 | 품목명 | 품종명 | 거래단위 | 등급 | 평년 평균가격(원) | 평균가격(원) | |
---|---|---|---|---|---|---|---|
0 | 201801상순 | 건고추 | 화건 | 30 kg | 상품 | 381666.666667 | 590000.0 |
1 | 201801중순 | 건고추 | 화건 | 30 kg | 상품 | 380809.666667 | 590000.0 |
2 | 201801하순 | 건고추 | 화건 | 30 kg | 상품 | 380000.000000 | 590000.0 |
3 | 201802상순 | 건고추 | 화건 | 30 kg | 상품 | 380000.000000 | 590000.0 |
4 | 201802중순 | 건고추 | 화건 | 30 kg | 상품 | 376666.666667 | 590000.0 |
1
train_sanji.head()
시점 | 공판장코드 | 공판장명 | 품목코드 | 품목명 | 품종코드 | 품종명 | 등급코드 | 등급명 | 총반입량(kg) | ... | 평균가(원/kg) | 중간가(원/kg) | 최저가(원/kg) | 최고가(원/kg) | 경매 건수 | 전순 평균가격(원) PreVious SOON | 전달 평균가격(원) PreVious MMonth | 전년 평균가격(원) PreVious YeaR | 평년 평균가격(원) Common Year SOON | 연도 | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 201801상순 | 1000000000 | *전국농협공판장 | 501 | 감자 | 50101 | 수미 | 11 | 특 | 15470.0 | ... | 1712.637363 | 1723.961039 | 1545.454545 | 2320.666667 | 7 | 1947.350427 | 1769.858320 | 1023.982379 | 0.0 | 2018 |
1 | 201801상순 | 1000000000 | *전국농협공판장 | 501 | 감자 | 50101 | 수미 | 12 | 상 | 2900.0 | ... | 1198.655172 | 1252.737207 | 893.055556 | 1417.857143 | 4 | 1301.239669 | 1348.253676 | 571.311475 | 0.0 | 2018 |
2 | 201801상순 | 1000000000 | *전국농협공판장 | 501 | 감자 | 50199 | 기타감자 | 13 | 보통 | 1320.0 | ... | 615.000000 | 600.000000 | 240.000000 | 911.875000 | 7 | 630.851064 | 449.166667 | 473.032787 | 0.0 | 2018 |
3 | 201801상순 | 1000000000 | *전국농협공판장 | 501 | 감자 | 50199 | 기타감자 | 12 | 상 | 460.0 | ... | 544.130435 | 365.000000 | 200.000000 | 1650.000000 | 5 | 1088.046875 | 1129.600000 | 734.024390 | 0.0 | 2018 |
4 | 201801상순 | 1000000000 | *전국농협공판장 | 501 | 감자 | 50199 | 기타감자 | 11 | 특 | 30967.0 | ... | 1876.454484 | 2010.440477 | 1598.327715 | 2438.720588 | 8 | 2126.402457 | 1779.262728 | 1750.544700 | 0.0 | 2018 |
5 rows × 21 columns
1
train_jeon.head()
시점 | 시장코드 | 시장명 | 품목코드 | 품목명 | 품종코드 | 품종명 | 총반입량(kg) | 총거래금액(원) | 평균가(원/kg) | ... | 저가(20%) 평균가 | 중간가(원/kg) | 최저가(원/kg) | 최고가(원/kg) | 경매 건수 | 전순 평균가격(원) PreVious SOON | 전달 평균가격(원) PreVious MMonth | 전년 평균가격(원) PreVious YeaR | 평년 평균가격(원) Common Year SOON | 연도 | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 201801상순 | 100000 | *전국도매시장 | 501 | 감자 | 50124 | 깐감자 | 20.0 | 86520 | 4326.000000 | ... | 4326.000000 | 4326.000000 | 4326.0 | 4326.000000 | 1 | 0.000000 | 4009.000000 | 0.000000 | 0.000000 | 2018 |
1 | 201801상순 | 100000 | *전국도매시장 | 501 | 감자 | 50121 | 돼지감자 | 12380.0 | 11650810 | 941.099354 | ... | 545.105717 | 1010.000000 | 200.0 | 3000.000000 | 117 | 11213.358450 | 9174.196723 | 8167.895632 | 0.000000 | 2018 |
2 | 201801상순 | 100000 | *전국도매시장 | 501 | 감자 | 50110 | 자주감자 | 240.0 | 158400 | 660.000000 | ... | 500.000000 | 550.000000 | 500.0 | 1000.000000 | 7 | 12553.279352 | 12612.216445 | 24990.324897 | 18483.961304 | 2018 |
3 | 201801상순 | 100000 | *전국도매시장 | 501 | 감자 | 50111 | 가을감자 | 10.0 | 37500 | 3750.000000 | ... | 3700.000000 | 3750.000000 | 3700.0 | 3800.000000 | 2 | 24929.463415 | 40365.081269 | 0.000000 | 0.000000 | 2018 |
4 | 201801상순 | 100000 | *전국도매시장 | 501 | 감자 | 50199 | 기타감자 | 1367301.3 | 2403199462 | 1757.622451 | ... | 955.289668 | 1360.453431 | 0.0 | 10581.081081 | 872 | 30806.779529 | 27661.150770 | 23741.953223 | 19340.121989 | 2018 |
5 rows × 22 columns
결측치 처리
1) 중위값
1
2
- 장점: 계산이 간단하고 이상치에 덜 민감합니다.
- 단점: 데이터의 분포를 고려하지 않고, 결측치가 많은 경우 정보 손실이 발생할 수 있습니다.
1
2
3
4
5
6
7
8
9
10
# # 1) 중위값으로 결측치 처리
# # 가격 정보가 0인 데이터들을 결측치로 변환
# train.replace({'평균가격(원)': 0}, pd.NA, inplace=True)
# train_sanji.replace({'평균가(원/kg)': 0}, pd.NA, inplace=True)
# train_jeon.replace({'평균가(원/kg)': 0}, pd.NA, inplace=True)
# # 품목별 중위값으로 결측치를 채움
# train['평균가격(원)'] = train.groupby('품목명')['평균가격(원)'].transform(lambda x: x.fillna(x.median()))
# train_sanji['평균가(원/kg)'] = train_sanji.groupby('품목명')['평균가(원/kg)'].transform(lambda x: x.fillna(x.median()))
# train_jeon['평균가(원/kg)'] = train_jeon.groupby('품목명')['평균가(원/kg)'].transform(lambda x: x.fillna(x.median()))
2) 보간법
1
2
- 장점: 주변 데이터를 활용하여 결측치를 추정하므로, 데이터의 추세를 반영할 수 있습니다.
- 단점: 결측치가 많은 경우 정확도가 떨어질 수 있고, 이상치에 민감합니다.
1
2
3
4
5
6
7
8
9
10
# # 2) 보간법으로 결측치 처리
# # 시간 순으로 정렬
# train.sort_values(by='시점', inplace=True)
# train_sanji.sort_values(by='시점', inplace=True)
# train_jeon.sort_values(by='시점', inplace=True)
# # 선형 보간법으로 결측치를 채움
# train['평균가격(원)'] = train.groupby('품목명')['평균가격(원)'].transform(lambda x: x.interpolate(method='linear'))
# train_sanji['평균가(원/kg)'] = train_sanji.groupby('품목명')['평균가(원/kg)'].transform(lambda x: x.interpolate(method='linear'))
# train_jeon['평균가(원/kg)'] = train_jeon.groupby('품목명')['평균가(원/kg)'].transform(lambda x: x.interpolate(method='linear'))
3) 이동 평균법
1
2
- 장점: 시계열 데이터의 추세를 반영하고, 노이즈를 줄여 결측치를 추정할 수 있습니다.
- 단점: window 크기에 따라 성능이 달라질 수 있고, 과거 데이터에 의존하여 미래 예측에 대한 정확도가 떨어질 수 있습니다.
1
2
3
4
5
# # 3) 이동 평균법으로 결측치 처리
# # 품목별 3개월 이동 평균으로 결측치를 채움
# train['평균가격(원)'] = train.groupby('품목명')['평균가격(원)'].transform(lambda x: x.fillna(x.rolling(window=3, center=True).mean()))
# train_sanji['평균가(원/kg)'] = train_sanji.groupby('품목명')['평균가(원/kg)'].transform(lambda x: x.fillna(x.rolling(window=3, center=True).mean()))
# train_jeon['평균가(원/kg)'] = train_jeon.groupby('품목명')['평균가(원/kg)'].transform(lambda x: x.fillna(x.rolling(window=3, center=True).mean()))
최적의 결측치 처리 방안
품목별 특성, 시점의 특성, 다른 변수와의 관계, 도메인 지식 등을 종합적으로 고려하여 결측치 처리
- 기본적으로 선형 보간법을 사용. 시계열 데이터의 추세를 반영하면서도, 중위값이나 이동 평균법보다 더 많은 정보를 활용할 수 있기 때문
- 결측치가 연속적으로 발생한 경우, 이동 평균법을 적용. 연속적인 결측치는 선형 보간법으로 처리하기 어려울 수 있으므로, 주변 데이터의 평균을 활용하는 이동 평균법을 사용
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# 가격 정보가 0인 데이터들을 결측치로 변환
train.replace({'평균가격(원)': 0}, pd.NA, inplace=True)
train_sanji.replace({'평균가(원/kg)': 0}, pd.NA, inplace=True)
train_jeon.replace({'평균가(원/kg)': 0}, pd.NA, inplace=True)
# 가격 정보가 0인 데이터들을 결측치로 변환
train['평균가격(원)'] = pd.to_numeric(train['평균가격(원)'], errors='coerce')
# '평균가(원/kg)' 열을 숫자형으로 변환
train_sanji['평균가(원/kg)'] = pd.to_numeric(train_sanji['평균가(원/kg)'], errors='coerce')
train_jeon['평균가(원/kg)'] = pd.to_numeric(train_jeon['평균가(원/kg)'], errors='coerce')
# 시간 순으로 정렬
train.sort_values(by='시점', inplace=True)
train_sanji.sort_values(by='시점', inplace=True)
train_jeon.sort_values(by='시점', inplace=True)
# 품목별 특성을 고려하여 결측치 처리
for 품목 in train['품목명'].unique():
# 선형 보간법 적용
train['평균가격(원)'] = train.groupby('품목명')['평균가격(원)'].transform(lambda x: x.interpolate(method='linear'))
train_sanji['평균가(원/kg)'] = train_sanji.groupby('품목명')['평균가(원/kg)'].transform(lambda x: x.interpolate(method='linear'))
train_jeon['평균가(원/kg)'] = train_jeon.groupby('품목명')['평균가(원/kg)'].transform(lambda x: x.interpolate(method='linear'))
# 결측치가 연속적으로 발생한 경우, 이동 평균법 적용
train['평균가격(원)'] = train.groupby('품목명')['평균가격(원)'].transform(lambda x: x.fillna(x.rolling(window=3, center=True).mean()))
train_sanji['평균가(원/kg)'] = train_sanji.groupby('품목명')['평균가(원/kg)'].transform(lambda x: x.fillna(x.rolling(window=3, center=True).mean()))
train_jeon['평균가(원/kg)'] = train_jeon.groupby('품목명')['평균가(원/kg)'].transform(lambda x: x.fillna(x.rolling(window=3, center=True).mean()))
이상치 처리 : 제거안함
- 농산물 가격은 작황, 수급 상황, 계절적 요인 등에 따라 변동 될수있어 사분위수 범위를 벗어난 가격이라 하더라도, 실제로는 정상적인 가격 변동일 가능성이 높다
- 그렇기때문에 이상치 제거시 농산물 가격 변동 정보가 손실될 수 있다. 농산물 가격에서 극단적인 가격 변동은 예측 모델 학습에 중요한 정보일 수 있다
- 이상치 제거로 훈련 데이터의 분포를 제한하면 과적합 위험이 발생될 수 있다
로그 변환, Box-Cox 변환 등을 사용하거나 윈저화(winsorizing)를 통해 극단적인 상위/하위 백분위수(5%이하, 95%이상 등) 값으로 대체 등의 이상치 처리를 고려할 수 있다
- 이상치 처리 대안
- 농산물 데이터는 이상치를 제거하거나 변환하는 것보다
robustscaler : 중앙값과 사분위수 범위를 사용하여 스케일링
등 스케일링을 추천
- 농산물 데이터는 이상치를 제거하거나 변환하는 것보다
스케일링 : RobustScaler 방식 사용
RobustScaler
는 중앙값과 사분위수 범위를 사용하여 스케일링하는 기법으로 데이터의 정보를 최대한 보존하면서 스케일링을 수행할 수 있다- 농산물 가격은 이상치가 많이 발생할 수 있기에, 이상치에 덜 민감한 robustscaler 방식을 사용하는 것이 적절해 보인다
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# # 정규화 스케일링
# from sklearn.preprocessing import MinMaxScaler
# # MinMaxScaler 객체 생성
# scaler = MinMaxScaler()
# # 스케일링 적용할 컬럼 선택
# num_cols = train.select_dtypes(include=['number']).columns
# # train 데이터에 MinMaxScaler 적용
# train[num_cols] = scaler.fit_transform(train[num_cols])
# # train_sanji 데이터에 MinMaxScaler 적용
# num_cols_sanji = train_sanji.select_dtypes(include=['number']).columns
# # '공판장코드', '품목코드', '품종코드', '등급코드', '연도' 열은 제외합니다.
# num_cols_sanji = num_cols_sanji.drop(['공판장코드', '품목코드', '품종코드', '등급코드', '연도'])
# train_sanji[num_cols_sanji] = scaler.fit_transform(train_sanji[num_cols_sanji])
# # train_jeon 데이터에 MinMaxScaler 적용
# num_cols_jeon = train_jeon.select_dtypes(include=['number']).columns
# # '시장코드', '품목코드', '품종코드', '연도' 열은 제외합니다.
# num_cols_jeon = num_cols_jeon.drop(['시장코드', '품목코드', '품종코드', '연도'])
# train_jeon[num_cols_jeon] = scaler.fit_transform(train_jeon[num_cols_jeon])
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# # 표준화 스케일링
# from sklearn.preprocessing import StandardScaler
# # StandardScaler 객체 생성
# scaler = StandardScaler()
# # 스케일링 적용할 컬럼 선택
# num_cols = train.select_dtypes(include=['number']).columns
# # train 데이터에 StandardScaler 적용
# train[num_cols] = scaler.fit_transform(train[num_cols])
# # train_sanji 데이터에 StandardScaler 적용
# num_cols_sanji = train_sanji.select_dtypes(include=['number']).columns
# # '공판장코드', '품목코드', '품종코드', '등급코드', '연도' 열은 제외합니다.
# num_cols_sanji = num_cols_sanji.drop(['공판장코드', '품목코드', '품종코드', '등급코드', '연도'])
# train_sanji[num_cols_sanji] = scaler.fit_transform(train_sanji[num_cols_sanji])
# # train_jeon 데이터에 StandardScaler 적용
# num_cols_jeon = train_jeon.select_dtypes(include=['number']).columns
# # '시장코드', '품목코드', '품종코드', '연도' 열은 제외합니다.
# num_cols_jeon = num_cols_jeon.drop(['시장코드', '품목코드', '품종코드', '연도'])
# train_jeon[num_cols_jeon] = scaler.fit_transform(train_jeon[num_cols_jeon])
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# robustscaler 스케일링
from sklearn.preprocessing import RobustScaler, LabelEncoder
# RobustScaler 객체 생성
scaler = RobustScaler()
# 스케일링 적용할 컬럼 선택
num_cols = train.select_dtypes(include=['number']).columns
# train 데이터에 RobustScaler 적용
train[num_cols] = scaler.fit_transform(train[num_cols])
# train_sanji 데이터에 RobustScaler 적용
num_cols_sanji = train_sanji.select_dtypes(include=['number']).columns
# '공판장코드', '품목코드', '품종코드', '등급코드', '연도' 열은 제외합니다.
num_cols_sanji = num_cols_sanji.drop(['공판장코드', '품목코드', '품종코드', '등급코드', '연도'])
train_sanji[num_cols_sanji] = scaler.fit_transform(train_sanji[num_cols_sanji])
# train_jeon 데이터에 RobustScaler 적용
num_cols_jeon = train_jeon.select_dtypes(include=['number']).columns
# '시장코드', '품목코드', '품종코드', '연도' 열은 제외합니다.
num_cols_jeon = num_cols_jeon.drop(['시장코드', '품목코드', '품종코드', '연도'])
train_jeon[num_cols_jeon] = scaler.fit_transform(train_jeon[num_cols_jeon])
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# 파생변수 생성
def create_date_features(df):
# '상순', '중순', '하순'을 숫자로 변환하는 함수
def convert_soon(soon):
if soon == '상순':
return '1'
elif soon == '중순':
return '11'
elif soon == '하순':
return '21'
else:
return soon # 숫자 형태 그대로 반환
# '시점' 열에서 '상순', '중순', '하순'을 숫자로 변환
df['시점'] = df['시점'].astype(str).str[:-2] + df['시점'].astype(str).str[-2:].apply(convert_soon)
# 변환된 '시점' 열을 datetime 형식으로 변환
df['시점'] = pd.to_datetime(df['시점'], format='%Y%m%d')
df['연도'] = df['시점'].dt.year
df['월'] = df['시점'].dt.month
df['순'] = df['시점'].dt.day // 10 + 1 # 1, 2, 3으로 변환
df['계절'] = (df['월'] // 3) % 4 # 0: 봄, 1: 여름, 2: 가을, 3: 겨울
df['요일'] = df['시점'].dt.dayofweek
return df
train = create_date_features(train)
train_sanji = create_date_features(train_sanji)
train_jeon = create_date_features(train_jeon)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 2. 품목 및 품종 관련 특징
encoder = LabelEncoder()
train['품목_인코딩'] = encoder.fit_transform(train['품목명'])
train['품종_인코딩'] = encoder.fit_transform(train['품종명'])
# 3. 가격 관련 특징
def create_price_features(df, col_name):
# 품목별, 연도별, 월별 평균 가격 계산
df['평균_가격'] = df.groupby(['품목명', '연도', '월'])[col_name].transform('mean')
# 전년 동월 대비 가격 변화율 계산
df['전년_동월_대비_가격_변화율'] = df.groupby(['품목명', '월'])[col_name].pct_change(periods=12)
# 전순 대비 가격 변화율 계산
df['전순_대비_가격_변화율'] = df.groupby(['품목명'])[col_name].pct_change(periods=1)
# 3개월 이동 평균 계산
df['이동_평균_3개월'] = df.groupby(['품목명'])[col_name].rolling(window=3, center=True).mean().reset_index(level=0, drop=True)
# 6개월 이동 평균 계산
df['이동_평균_6개월'] = df.groupby(['품목명'])[col_name].rolling(window=6, center=True).mean().reset_index(level=0, drop=True)
return df
train = create_price_features(train, '평균가격(원)')
train_sanji = create_price_features(train_sanji, '평균가(원/kg)')
train_jeon = create_price_features(train_jeon, '평균가(원/kg)')
1
2
3
4
<ipython-input-48-19f3df8e9be8>:12: FutureWarning: The default fill_method='ffill' in SeriesGroupBy.pct_change is deprecated and will be removed in a future version. Either fill in any non-leading NA values prior to calling pct_change or specify 'fill_method=None' to not fill NA values.
df['전년_동월_대비_가격_변화율'] = df.groupby(['품목명', '월'])[col_name].pct_change(periods=12)
<ipython-input-48-19f3df8e9be8>:15: FutureWarning: The default fill_method='ffill' in SeriesGroupBy.pct_change is deprecated and will be removed in a future version. Either fill in any non-leading NA values prior to calling pct_change or specify 'fill_method=None' to not fill NA values.
df['전순_대비_가격_변화율'] = df.groupby(['품목명'])[col_name].pct_change(periods=1)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 4. 기타 특징
def create_other_features(df):
# 품목별 평균 총반입량 계산
df['평균_총반입량'] = df.groupby('품목명')['총반입량(kg)'].transform('mean')
# 전년 동월 대비 총반입량 변화율 계산
df['전년_동월_대비_총반입량_변화율'] = df.groupby(['품목명', '월'])['총반입량(kg)'].pct_change(periods=12)
# 품목별 평균 경매 건수 계산
df['평균_경매_건수'] = df.groupby('품목명')['경매 건수'].transform('mean')
# 전년 동월 대비 경매 건수 변화율 계산
df['전년_동월_대비_경매_건수_변화율'] = df.groupby(['품목명', '월'])['경매 건수'].pct_change(periods=12)
return df
train_sanji = create_other_features(train_sanji)
train_jeon = create_other_features(train_jeon)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 데이터 필터링
def filter_data(df, item_name):
if item_name == '무':
df = df[df['품종명'].isin(['봄무', '여름무', '가을무', '기타무', '다발무'])]
elif item_name == '사과':
df = df[df['품종명'].isin(['홍로', '후지'])]
elif item_name == '양파':
df = df[df['품종명'].isin(['양파', '기타양파'])]
elif item_name == '배추':
df = df[df['품종명'].isin(['배추', '쌈배추'])]
elif item_name == '건고추':
df = df[df['품종명'].isin(['화건', '꼭지건고추'])]
elif item_name == '깐마늘(국산)':
df = df[df['품종명'].isin(['깐마늘(국산)', '깐마늘'])]
elif item_name == '대파':
df = df[df['품종명'].isin(['대파', '대파(일반)'])]
elif item_name == '감자':
df = df[df['품종명'].isin(['감자 수미', '수미'])]
elif item_name == '배':
df = df[df['품종명'].isin(['신고', '기타배'])]
elif item_name == '상추':
df = df[df['품종명'].isin(['청상추', '청', '적상추'])]
return df
This post is licensed under CC BY 4.0 by the author.