Post

데이콘 프로젝트 - 농산물 가격예측 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()
시점품목명품종명거래단위등급평년 평균가격(원)평균가격(원)
0201801상순건고추화건30 kg상품381666.666667590000.0
1201801중순건고추화건30 kg상품380809.666667590000.0
2201801하순건고추화건30 kg상품380000.000000590000.0
3201802상순건고추화건30 kg상품380000.000000590000.0
4201802중순건고추화건30 kg상품376666.666667590000.0
1
train_sanji.head()
시점공판장코드공판장명품목코드품목명품종코드품종명등급코드등급명총반입량(kg)...평균가(원/kg)중간가(원/kg)최저가(원/kg)최고가(원/kg)경매 건수전순 평균가격(원) PreVious SOON전달 평균가격(원) PreVious MMonth전년 평균가격(원) PreVious YeaR평년 평균가격(원) Common Year SOON연도
0201801상순1000000000*전국농협공판장501감자50101수미1115470.0...1712.6373631723.9610391545.4545452320.66666771947.3504271769.8583201023.9823790.02018
1201801상순1000000000*전국농협공판장501감자50101수미122900.0...1198.6551721252.737207893.0555561417.85714341301.2396691348.253676571.3114750.02018
2201801상순1000000000*전국농협공판장501감자50199기타감자13보통1320.0...615.000000600.000000240.000000911.8750007630.851064449.166667473.0327870.02018
3201801상순1000000000*전국농협공판장501감자50199기타감자12460.0...544.130435365.000000200.0000001650.00000051088.0468751129.600000734.0243900.02018
4201801상순1000000000*전국농협공판장501감자50199기타감자1130967.0...1876.4544842010.4404771598.3277152438.72058882126.4024571779.2627281750.5447000.02018

5 rows × 21 columns

1
train_jeon.head()
시점시장코드시장명품목코드품목명품종코드품종명총반입량(kg)총거래금액(원)평균가(원/kg)...저가(20%) 평균가중간가(원/kg)최저가(원/kg)최고가(원/kg)경매 건수전순 평균가격(원) PreVious SOON전달 평균가격(원) PreVious MMonth전년 평균가격(원) PreVious YeaR평년 평균가격(원) Common Year SOON연도
0201801상순100000*전국도매시장501감자50124깐감자20.0865204326.000000...4326.0000004326.0000004326.04326.00000010.0000004009.0000000.0000000.0000002018
1201801상순100000*전국도매시장501감자50121돼지감자12380.011650810941.099354...545.1057171010.000000200.03000.00000011711213.3584509174.1967238167.8956320.0000002018
2201801상순100000*전국도매시장501감자50110자주감자240.0158400660.000000...500.000000550.000000500.01000.000000712553.27935212612.21644524990.32489718483.9613042018
3201801상순100000*전국도매시장501감자50111가을감자10.0375003750.000000...3700.0000003750.0000003700.03800.000000224929.46341540365.0812690.0000000.0000002018
4201801상순100000*전국도매시장501감자50199기타감자1367301.324031994621757.622451...955.2896681360.4534310.010581.08108187230806.77952927661.15077023741.95322319340.1219892018

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.