🔥알림🔥
① 테디노트 유튜브 - 구경하러 가기!
② LangChain 한국어 튜토리얼 바로가기 👀
③ 랭체인 노트 무료 전자책(wikidocs) 바로가기 🙌

26 분 소요

나라장터에서 토건, 토목 등 다양한 시공에 대한 입찰 공고를 고지합니다.

시공사들은 예가에 가장 가깝게 입찰하여, 공사를 수주하게 됩니다.

시공사는 최대한 많은 공사를 입찰 받을 수록 회사에 큰 도움이 되기 때문에, 열심히 전자 입찰에 대한 스터디까지 진행하면서, 공사를 수주하기 위해 안간힘을 쓰고 있습니다.

투명하고 공정한 방식으로 입찰을 진행하기 때문에 거의 모든 업체에게 공평하게 기회가 돌아가야하지만, 입찰을 굉장히 많이 받는 업체가 존재합니다.

단순하게 이 엄청 좋아서일까요?

궁금증을 해소해 보고자 한 번 분석을 진행하였습니다.

그리고, 재미삼아 백 테스팅을 통하여 모의 입찰도 진행해 봤습니다.

성공적인 입찰을 위한 데이터 분석 (토건, 토목, 시공 편)

주요 용어 정리

  • 기초금액: 공사 입찰을 위한 기준 금액 (부가가치세가 합산된 금액)
  • 예가: 기초금액±3% 으로 형성되며, 이 안에 들어오는 금액으로 입찰 해야함.(결국, 예가에 근접한 가격을 입찰해야함)
  • 예정가격: 예가(%)를 기초금액에 곱한 가격
  • 사정율: 예정가격을 기준으로 예정가격을 예측하는 비율. (1등 선정된 업체의 예가)

그 외 다소 생소한 용어들이 존재하나, 분석에 있어 주요한 용어만 설명합니다.

입찰 선정 방식

예가에 가장 근접한 기업이 입찰 받음. 단, 예가보다는 높은 금액을 입찰해야 합니다.

예시

기초금액: 1억원

입찰급액

  • A기업: 9900만원
  • B기업: 1억 1000만원
  • C기업: 1억 2000만원

사정율

  • A기업: 99.0%, (1억원 대비 99.0% 이므로)
  • B기업: 101.0%, (1억원 대비 101.0% 이므로)
  • C기업: 102.0%, (1억원 대비 102.0% 이므로)

시나리오 1) 예가: 1억 1500만원

  • C기업 입찰. B기업이 예가에는 더 근접한 금액을 제시했지만, 예가보다 낮으므로, C기업이 입찰 받습니다.

데이터 분석 방식

  1. 나라장터에서 필요한 데이터 크롤링
  2. pandas, numpy, matplotlib등을 활용하여 분석 및 시각화 진행
  3. sklearn, xgboost, lgbm을 활용하여 데이터 예측 수행
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import warnings
import matplotlib as mpl

# 유니코드 깨짐현상 해결
mpl.rcParams['axes.unicode_minus'] = False

# 나눔고딕 폰트 적용
plt.rcParams["font.family"] = 'NanumGothic'

# 경고 무시
warnings.filterwarnings('ignore')
%matplotlib inline

데이터 로드

train =  pd.read_csv('data/train.csv')

컬럼 출력

train.columns
Index(['번호', '공고업종', '지역', '공고번호', '발주기관', '입찰일시', '기초금액', '추정가격', '낙찰하한율',
       '예정가격', '예가', '낙찰하한가', '투찰', '업체명', '대표자', '투찰금액', '투찰율', '기초대비',
       '업체별사정율', 'num_1_num', 'num_1_money', 'num_1_per', 'num_2_num',
       'num_2_money', 'num_2_per', 'num_3_num', 'num_3_money', 'num_3_per',
       'num_4_num', 'num_4_money', 'num_4_per', 'num_5_num', 'num_5_money',
       'num_5_per', 'num_6_num', 'num_6_money', 'num_6_per', 'num_7_num',
       'num_7_money', 'num_7_per', 'num_8_num', 'num_8_money', 'num_8_per',
       'num_9_num', 'num_9_money', 'num_9_per', 'num_10_num', 'num_10_money',
       'num_10_per', 'num_11_num', 'num_11_money', 'num_11_per', 'num_12_num',
       'num_12_money', 'num_12_per', 'num_13_num', 'num_13_money',
       'num_13_per', 'num_14_num', 'num_14_money', 'num_14_per', 'num_15_num',
       'num_15_money', 'num_15_per', 'total', 'Unnamed: 65', 'Unnamed: 66'],
      dtype='object')

1. 예가

train['예가'].head()
0    99.4075 % (-0.5924 % )
1    99.2730 % (-0.7269 % )
2    99.1207 % (-0.8792 % )
3    99.8778 % (-0.1221 % )
4    99.6818 % (-0.3181 % )
Name: 예가, dtype: object

예가에 대한 전처리 작업 수행. 필요한 숫자 소수점 4째자리 까지 추출

train['예가'] = train['예가'].str.extract(r'(\d+[.]\d+)')

예가를 object -> float로 변환

# float로 변경
train['예가'] = train['예가'].astype('float32')

예가는 97 ~ 103의 범위를 가져야 함.

이에 어긋나는 데이터는 drop

train.loc[(train['예가'] < 97) | (train['예가'] > 103), '예가']
7377      0.000000
8646      0.000000
9522      0.000000
10945    90.908997
15187    59.200001
15268     0.000000
16133     0.000000
16382     0.000000
16543     0.000000
17183     0.000000
25247    96.362503
29316     0.000000
Name: 예가, dtype: float32
drop_idx = train.loc[(train['예가'] < 97) | (train['예가'] > 103), '예가'].index
train = train.drop(drop_idx)

예가가 NaN인 값을 확인하고 drop

train.loc[train['예가'].isnull()]
번호 공고업종 지역 공고번호 발주기관 입찰일시 기초금액 추정가격 낙찰하한율 예정가격 ... num_13_per num_14_num num_14_money num_14_per num_15_num num_15_money num_15_per total Unnamed: 65 Unnamed: 66
31788 NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN ... 99.890635 7214 NaN 99.905027 7188 NaN 99.899742 NaN NaN 99.891641

1 rows × 67 columns

drop_idx = train.loc[train['예가'].isnull()].index
train = train.drop(drop_idx)

예가에 대한 데이터 분포 확인

sns.distplot(train['예가'])
<matplotlib.axes._subplots.AxesSubplot at 0x7f5f01cfc3c8>
train['예가'].describe()
count    31776.000000
mean        99.905418
std          0.732136
min         97.069199
25%         99.406502
50%         99.912399
75%        100.410723
max        102.201698
Name: 예가, dtype: float64

2. 가격

train.filter(regex='금액|가격').head()
기초금액 추정가격 예정가격 투찰금액
0 166,200,000 원 151,090,909 원 165,215,275 원 145,039,600
1 49,000,000 원 44,545,455 원 48,643,800 원 42,786,020
2 35,000,000 원 31,818,182 원 34,692,275 원 30,530,000
3 27,000,000 원 24,545,455 원 26,967,025 원 23,688,450
4 64,460,000 원 58,600,000 원 64,254,925 원 56,382,000

가격에 대한 단위를 제거 및 ,를 제거합니다.

price_cols = train.filter(regex='금액|가격').columns
price_cols
Index(['기초금액', '추정가격', '예정가격', '투찰금액'], dtype='object')
train[price_cols].head()
기초금액 추정가격 예정가격 투찰금액
0 166,200,000 원 151,090,909 원 165,215,275 원 145,039,600
1 49,000,000 원 44,545,455 원 48,643,800 원 42,786,020
2 35,000,000 원 31,818,182 원 34,692,275 원 30,530,000
3 27,000,000 원 24,545,455 원 26,967,025 원 23,688,450
4 64,460,000 원 58,600,000 원 64,254,925 원 56,382,000

,를 제거합니다.

for col in price_cols:
    train[col] = train[col].str.replace(',', '')
    train[col] = train[col].str.replace('원', '')
train[price_cols].head()
기초금액 추정가격 예정가격 투찰금액
0 166200000 151090909 165215275 145039600
1 49000000 44545455 48643800 42786020
2 35000000 31818182 34692275 30530000
3 27000000 24545455 26967025 23688450
4 64460000 58600000 64254925 56382000

NaN값을 채워줍니다.

type을 int로 변경하기 전 NaN 값을 채워줍니다.

train[price_cols].isnull().sum()
기초금액     0
추정가격    66
예정가격     0
투찰금액     0
dtype: int64
train[price_cols] = train[price_cols].fillna(0)
train[price_cols].isnull().sum()
기초금액    0
추정가격    0
예정가격    0
투찰금액    0
dtype: int64
train[price_cols].head()
기초금액 추정가격 예정가격 투찰금액
0 166200000 151090909 165215275 145039600
1 49000000 44545455 48643800 42786020
2 35000000 31818182 34692275 30530000
3 27000000 24545455 26967025 23688450
4 64460000 58600000 64254925 56382000
train[price_cols].info()
<class 'pandas.core.frame.DataFrame'>
Int64Index: 31776 entries, 0 to 31787
Data columns (total 4 columns):
기초금액    31776 non-null object
추정가격    31776 non-null object
예정가격    31776 non-null object
투찰금액    31776 non-null object
dtypes: object(4)
memory usage: 1.2+ MB

float 타입으로 가격 컬럼을 변환

train[price_cols] = train[price_cols].astype('float32')
train[price_cols].head()
기초금액 추정가격 예정가격 투찰금액
0 166200000.0 151090912.0 165215280.0 145039600.0
1 49000000.0 44545456.0 48643800.0 42786020.0
2 35000000.0 31818182.0 34692276.0 30530000.0
3 27000000.0 24545456.0 26967024.0 23688450.0
4 64460000.0 58600000.0 64254924.0 56382000.0
train[price_cols].info()
<class 'pandas.core.frame.DataFrame'>
Int64Index: 31776 entries, 0 to 31787
Data columns (total 4 columns):
기초금액    31776 non-null float32
추정가격    31776 non-null float32
예정가격    31776 non-null float32
투찰금액    31776 non-null float32
dtypes: float32(4)
memory usage: 744.8 KB

NaN 값을 drop 하였으므로, index 를 초기화한다

train = train.reset_index(drop=True)

명시된 예정 가격과 예가 비율 * 기초금액과 가격이 과연 같을까?

train['기초금액'][:5]
0    166200000.0
1     49000000.0
2     35000000.0
3     27000000.0
4     64460000.0
Name: 기초금액, dtype: float32
train['예정가격'][:5]
0    165215280.0
1     48643800.0
2     34692276.0
3     26967024.0
4     64254924.0
Name: 예정가격, dtype: float32
train['예가'][:5]
0    99.407501
1    99.273003
2    99.120697
3    99.877800
4    99.681801
Name: 예가, dtype: float32

기초금액 x 예가 = 예정가격 정말 일치할까? (왜냐하면, 소수 4째짜리까지만 반영되기 때문에 다를 수 있다)

10개만 테스트해 보겠다.

for i in range(10):
    calculated_price = train['기초금액'][i] * train['예가'][i] / 100
    printed_price = train['예정가격'][i]
    diff = abs(calculated_price - printed_price)
    print('명시된 가격과 계산된 금액의 차이: {:.2f} 원'.format(diff))
명시된 가격과 계산된 금액의 차이: 17.28 원
명시된 가격과 계산된 금액의 차이: 26.56 원
명시된 가격과 계산된 금액의 차이: 31.52 원
명시된 가격과 계산된 금액의 차이: 17.28 원
명시된 가격과 계산된 금액의 차이: 35.04 원
명시된 가격과 계산된 금액의 차이: 46.72 원
명시된 가격과 계산된 금액의 차이: 157.44 원
명시된 가격과 계산된 금액의 차이: 123.52 원
명시된 가격과 계산된 금액의 차이: 18.72 원
명시된 가격과 계산된 금액의 차이: 143.36 원

전체 평균 차이 계산

total_size = len(train)
diff = 0
for i in range(total_size):
    calculated_price = train['기초금액'][i] * train['예가'][i] / 100
    printed_price = train['예정가격'][i]
    diff += abs(calculated_price - printed_price)
diff /= total_size
print('평균 오차 가격: {:.2f} 원'.format(diff))
평균 오차 가격: 358.07 원

금액&가격 컬럼간 corr() 확인

column들이 매우 높은 상관관계를 가지고 있다.

따라서, 이중 하나의 컬럼만 지정해서 활용하면 될 것으로 보입니다.

가장 base가 되는 기초금액 컬럼을 활용하겠습니다.

train[price_cols].corr()
기초금액 추정가격 예정가격 투찰금액
기초금액 1.000000 0.998769 0.999979 0.998075
추정가격 0.998769 1.000000 0.998765 0.996852
예정가격 0.999979 0.998765 1.000000 0.998081
투찰금액 0.998075 0.996852 0.998081 1.000000
sns.heatmap(train[price_cols].corr(), annot=True)
<matplotlib.axes._subplots.AxesSubplot at 0x7f5f017f0358>

3. 날짜

train['입찰일시'].head()
0    2020-03-13 16:00
1    2020-03-13 16:00
2    2020-03-13 16:00
3    2020-03-13 16:00
4    2020-03-13 16:00
Name: 입찰일시, dtype: object

NaN 값 확인

train['입찰일시'].isnull().sum()
0

날짜를 변환 (datetime)

train['입찰일시'] = pd.to_datetime(train['입찰일시'])
train['year'] = train['입찰일시'].dt.year
train['month'] = train['입찰일시'].dt.month
train['day'] = train['입찰일시'].dt.day
train['hour'] = train['입찰일시'].dt.hour
train['minute'] = train['입찰일시'].dt.minute
train['dayofweek'] = train['입찰일시'].dt.dayofweek
train['weekofyear'] = train['입찰일시'].dt.weekofyear
train['dayofyear'] = train['입찰일시'].dt.dayofyear
train['quarter'] = train['입찰일시'].dt.quarter

연도 확인

plt.figure(figsize=(10, 6))
sns.countplot(train['year'])
plt.title('연도별 공고 현황', fontsize=18)
plt.show()

분기별 공고 현황

plt.figure(figsize=(10, 6))
sns.countplot(train['quarter'])
plt.title('분기별 공고 현황', fontsize=18)
plt.show()

월별 공고 현황

plt.figure(figsize=(10, 6))
sns.countplot(train['month'])
plt.title('월별 공고 현황', fontsize=18)
plt.show()

일별 공고 현황

plt.figure(figsize=(10, 6))
sns.countplot(train['day'])
plt.title('일자별 공고 현황', fontsize=18)
plt.show()

시간대별 공고 현황

plt.figure(figsize=(10, 6))
sns.countplot(train['hour'])
plt.title('시간대별 공고 현황', fontsize=18)
plt.show()

분(minute)별 공고 현황

plt.figure(figsize=(10, 6))
sns.countplot(train['minute'])
plt.title('분별 공고 현황', fontsize=18)
plt.show()

요일별 공고 현황

  • 0: 월요일 ~ 6: 일요일
  • 일요일은 역시 공고가 없다.
  • 토요일은 공고가 2건 있었다.
plt.figure(figsize=(10, 6))
sns.countplot(train['dayofweek'])
plt.title('요일별 공고 현황', fontsize=18)
plt.show()

4. 지역

train['지역'].value_counts()
전남                4141
경북                3884
경기                3756
경남                3480
강원                3259
전북                2544
충남                2204
충북                1631
서울                1608
부산                 821
제주                 813
대구                 729
인천                 722
대전                 425
광주                 344
울산                 338
전국                 240
세종                 138
전국,경기              128
전국,경북               58
전국,서울               57
전국,전남               44
전국,전북               38
전국,경남               37
서울,경기               36
전국,충남               31
전국,충북               28
전국,강원               27
전국,부산               23
전국,인천               23
                  ... 
전국,세종               12
충남,세종                9
전국,대전                8
전국,제주                8
부산,경남                7
인천,경기                5
서울,인천,경기             5
전북,전남                3
충남,전북                3
대전,충북,충남,세종          3
대전,충북,세종             2
대전,충남                2
대전,충북,충남             2
인천,경기,전남             1
부산,울산                1
울산,경남                1
충북,충남,전북             1
서울,경기,강원             1
경남,전남                1
전국,서울,경기             1
경기,강원                1
경기,강원,충북             1
대구,경북,경남             1
강원,경북,전남             1
부산,울산,경북             1
전국,울산,경남             1
강원,충북                1
광주,전북,전남             1
서울,인천                1
부산,대구,울산,경북,경남       1
Name: 지역, Length: 65, dtype: int64

지역을 , 기준으로 분리합니다.

area = train['지역'].str.split(',')
area.head()
0    [경기]
1    [충남]
2    [충남]
3    [강원]
4    [전북]
Name: 지역, dtype: object

최소 값과, 최대 값을 살펴봅니다.

area_count = area.apply(lambda x: len(x))
area_count.min(), area_count.max()
(1, 5)

각각의 데이터를 분리하여 column을 만들도록 합니다.

area_split = train["지역"].str.split(",", n=5, expand=True) 
area_split.iloc[[345, 12390, 1646, 16326]]
0 1 2 3 4
345 전국 None None None None
12390 대전 충남 None None None
1646 전국 울산 경남 None None
16326 부산 대구 울산 경북 경남
area_key = list(train['지역'].value_counts()[:18].keys())
area_key
['전남',
 '경북',
 '경기',
 '경남',
 '강원',
 '전북',
 '충남',
 '충북',
 '서울',
 '부산',
 '제주',
 '대구',
 '인천',
 '대전',
 '광주',
 '울산',
 '전국',
 '세종']
data = np.zeros(shape=(len(area), 18))
data.shape
(31776, 18)
area_df = pd.DataFrame(columns=area_key, data=data)

지역과 지역 값 컬럼을 합칩니다

area_merged = pd.concat([area, area_df], axis=1)
area_merged.tail()
지역 전남 경북 경기 경남 강원 전북 충남 충북 서울 부산 제주 대구 인천 대전 광주 울산 전국 세종
31771 [경남] 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
31772 [전국] 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
31773 [전국] 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
31774 [충북] 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
31775 [경남] 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0

list에 지역이 포함된 경우 1로 체크합니다

def check_area(data):
    for d in data['지역']:
        data[d] = 1
    return data
area = area_merged.apply(check_area, axis=1)
area.head()
지역 전남 경북 경기 경남 강원 전북 충남 충북 서울 부산 제주 대구 인천 대전 광주 울산 전국 세종
0 [경기] 0.0 0.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
1 [충남] 0.0 0.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
2 [충남] 0.0 0.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
3 [강원] 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
4 [전북] 0.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
area.iloc[1646]
지역    [전국, 울산, 경남]
전남               0
경북               0
경기               0
경남               1
강원               0
전북               0
충남               0
충북               0
서울               0
부산               0
제주               0
대구               0
인천               0
대전               0
광주               0
울산               1
전국               1
세종               0
Name: 1646, dtype: object

지역은 drop합니다

area = area.drop('지역', 1)
area.head()
전남 경북 경기 경남 강원 전북 충남 충북 서울 부산 제주 대구 인천 대전 광주 울산 전국 세종
0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
1 0.0 0.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
2 0.0 0.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
3 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
4 0.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
area.shape
(31776, 18)
train.shape
(31776, 76)

5. 업체별 사정율

사정율이란 개찰후 결정된 예정가격과 예비가격기초금액같의 차이를 백분율로 나타낸것이라고 정의 할 수 있습니다.

법률적인 개념은 아니지만 입찰담당자들 사이에 널리 쓰이는 개념입니다.

예를 들어

기초금액 : 100억

예정가격 : 101억이라면

사정율을 1%라고 합니다.

즉, 표면적으로 보이는 투찰율은 다르지만 사정율을 계산해서 보면 패턴을 볼 수 있다는 것이지요.

특정발주처나 특정업체의 입찰 패턴을 분석할 때 투찰율을 분석하면 아무런 결과를 얻을 수가 없습니다.

그러나 사정율로 분석을 하게 되면 패턴을 발견할 수도 있습니다.

복수예비가격을 사전에 발표하는 발주처의 성향을 분석할때도 금액만으로는 원하는 결과를 얻을수 없습니다.

각각의 예비가격에 대한 사정율을 계산해서 일정기간 분석해 보면 패턴이 있음을 알 수 있습니다.

train['업체별사정율'].head()
0    99.4565
1    99.5138
2    99.4114
3    99.9886
4    99.6845
Name: 업체별사정율, dtype: object
train.loc[train['업체별사정율'].str.contains('-'), '업체별사정율'].head(10)
288      ( - )
345      ( - )
854      ( - )
1046     ( - )
2227     ( - )
2292     ( - )
2764     ( - )
2765     ( - )
2798     ( - )
3056     ( - )
Name: 업체별사정율, dtype: object

( - ) 이루어진 데이터는 0으로 치환합니다

train.loc[train['업체별사정율'].str.contains('-'), '업체별사정율'] = np.nan
train['업체별사정율'] = train['업체별사정율'].fillna(0)

업체별 사정율의 데이터를 float로 변경합니다

train['업체별사정율'] = train['업체별사정율'].astype('float32')

7. 불필요한 컬럼 정리

train.columns
Index(['번호', '공고업종', '지역', '공고번호', '발주기관', '입찰일시', '기초금액', '추정가격', '낙찰하한율',
       '예정가격', '예가', '낙찰하한가', '투찰', '업체명', '대표자', '투찰금액', '투찰율', '기초대비',
       '업체별사정율', 'num_1_num', 'num_1_money', 'num_1_per', 'num_2_num',
       'num_2_money', 'num_2_per', 'num_3_num', 'num_3_money', 'num_3_per',
       'num_4_num', 'num_4_money', 'num_4_per', 'num_5_num', 'num_5_money',
       'num_5_per', 'num_6_num', 'num_6_money', 'num_6_per', 'num_7_num',
       'num_7_money', 'num_7_per', 'num_8_num', 'num_8_money', 'num_8_per',
       'num_9_num', 'num_9_money', 'num_9_per', 'num_10_num', 'num_10_money',
       'num_10_per', 'num_11_num', 'num_11_money', 'num_11_per', 'num_12_num',
       'num_12_money', 'num_12_per', 'num_13_num', 'num_13_money',
       'num_13_per', 'num_14_num', 'num_14_money', 'num_14_per', 'num_15_num',
       'num_15_money', 'num_15_per', 'total', 'Unnamed: 65', 'Unnamed: 66',
       'year', 'month', 'day', 'hour', 'minute', 'dayofweek', 'weekofyear',
       'dayofyear', 'quarter'],
      dtype='object')
cols = ['업체명', 
        '기초금액', 
        '예가', 
        '업체별사정율',
        'year', 
        'month', 
        'day', 
        'hour', 
        'minute', 
        'dayofweek', 
        'weekofyear', 
        'dayofyear', 
        'quarter', 
       ]
train[cols].head()
업체명 기초금액 예가 업체별사정율 year month day hour minute dayofweek weekofyear dayofyear quarter
0 제이티건설(주) 166200000.0 99.407501 99.456497 2020 3 13 16 0 4 11 73 1
1 통일종합건설 주식회사 49000000.0 99.273003 99.513802 2020 3 13 16 0 4 11 73 1
2 성문건설 주식회사 35000000.0 99.120697 99.411400 2020 3 13 16 0 4 11 73 1
3 (주)덕산종합건설 27000000.0 99.877800 99.988602 2020 3 13 16 0 4 11 73 1
4 (유)아라야 64460000.0 99.681801 99.684502 2020 3 13 16 0 4 11 73 1
train[cols].info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 31776 entries, 0 to 31775
Data columns (total 13 columns):
업체명           31776 non-null object
기초금액          31776 non-null float32
예가            31776 non-null float32
업체별사정율        31776 non-null float32
year          31776 non-null int64
month         31776 non-null int64
day           31776 non-null int64
hour          31776 non-null int64
minute        31776 non-null int64
dayofweek     31776 non-null int64
weekofyear    31776 non-null int64
dayofyear     31776 non-null int64
quarter       31776 non-null int64
dtypes: float32(3), int64(9), object(1)
memory usage: 2.8+ MB

df에 정리된 train 데이터와 area 데이터를 합칩니다

df = pd.concat([train[cols], area], axis=1)
df.head()
업체명 기초금액 예가 업체별사정율 year month day hour minute dayofweek ... 서울 부산 제주 대구 인천 대전 광주 울산 전국 세종
0 제이티건설(주) 166200000.0 99.407501 99.456497 2020 3 13 16 0 4 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
1 통일종합건설 주식회사 49000000.0 99.273003 99.513802 2020 3 13 16 0 4 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
2 성문건설 주식회사 35000000.0 99.120697 99.411400 2020 3 13 16 0 4 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
3 (주)덕산종합건설 27000000.0 99.877800 99.988602 2020 3 13 16 0 4 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
4 (유)아라야 64460000.0 99.681801 99.684502 2020 3 13 16 0 4 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0

5 rows × 31 columns

지역도 잘 합쳐져 있는지 확인합니다

df.iloc[:, 13:].tail()
전남 경북 경기 경남 강원 전북 충남 충북 서울 부산 제주 대구 인천 대전 광주 울산 전국 세종
31771 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
31772 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0 0.0
31773 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0 0.0
31774 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
31775 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
train['지역'].tail()
31771    경남
31772    전국
31773    전국
31774    충북
31775    경남
Name: 지역, dtype: object

업체별사정율 -> 사정율 (컬럼명 변환)

df = df.rename(columns={'업체별사정율': '사정율'})
df.head()
업체명 기초금액 예가 사정율 year month day hour minute dayofweek ... 서울 부산 제주 대구 인천 대전 광주 울산 전국 세종
0 제이티건설(주) 166200000.0 99.407501 99.456497 2020 3 13 16 0 4 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
1 통일종합건설 주식회사 49000000.0 99.273003 99.513802 2020 3 13 16 0 4 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
2 성문건설 주식회사 35000000.0 99.120697 99.411400 2020 3 13 16 0 4 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
3 (주)덕산종합건설 27000000.0 99.877800 99.988602 2020 3 13 16 0 4 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
4 (유)아라야 64460000.0 99.681801 99.684502 2020 3 13 16 0 4 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0

5 rows × 31 columns

8. 사정율 - 예가 컬럼 생성 (1등 업체의 근접도 확인)

df['근접율'] = abs(df['예가'] - df['사정율'])
plt.figure(figsize=(10, 6))
sns.distplot(df['근접율'])
plt.title('근접율 분포도', fontsize=18)
plt.show()
df.loc[df['근접율'] > 30, '근접율'].head(20)
288     100.150597
345      99.907997
854      99.307800
1046    100.894501
2227     99.351601
2292    100.069702
2764     98.986198
2765    100.400200
2798    100.603203
3056     99.776497
3298    100.022202
3972    100.263702
4044    100.124496
4242     99.884697
4591    100.806999
5824    100.657700
5826    100.247002
6035     99.893700
6185    100.836197
6189    100.503700
Name: 근접율, dtype: float32

사정율이 나와 있지 않은 column은 우리가 0으로 채워 주었기 때문에 100 이상의 수치가 나온다.

해당 컬럼은 제외하고 다시 시각화

plt.figure(figsize=(10, 6))
sns.distplot(df.loc[df['근접율'] < 1, '근접율'])
plt.title('근접율 분포도', fontsize=18)
plt.show()

선정된 대다수의 업체가 근접율을 0.0 ~ 0.2 이하로 가져가고 있다는 점을 볼 수 있다

8. 상위 업체 사정율 분석

주로 활용할 유의미한 feature 정의

  • 예가 (예측 label)
  • 일자 (datetime) 관련 column

안타깝께도... 기타 활용할만한 지표는 크게 보이지 않는다.

잘 되는 업체의 사정율 확인

df['업체명'].value_counts().head(50)
태양종합건설 주식회사      28
신우건설 주식회사        28
남광건설 주식회사        27
신영종합건설(주)        25
주식회사 우리종합건설      25
일진건설 주식회사        25
동우건설 주식회사        24
삼진건설(주)          24
주식회사 혜성종합건설      23
보광종합건설 주식회사      23
우일종합건설(주)        22
효창건설(주)          22
청암종합건설 주식회사      22
영진건설(주)          22
대연종합건설 주식회사      22
거성종합건설(주)        21
세움종합건설 주식회사      21
세원종합건설 주식회사      21
우진종합건설(주)        20
두마종합건설주식회사       20
주식회사 부림종합건설      20
주식회사 덕성건설        20
우림종합건설(주)        20
서림종합건설 주식회사      20
유창건설 주식회사        20
(주)동일종합건설        20
대림종합건설 주식회사      20
예림종합건설(주)        20
청진건설주식회사         20
주식회사 태화건설        19
대창건설(주)          19
성원건설(주)          19
승원종합건설(주)        19
제이와이건설 주식회사      19
금종종합건설 주식회사      19
(주)거성종합건설        19
주식회사 에스엠건설       19
성인종합건설 주식회사      19
대호종합건설(주)        19
대원건설(주)          18
주식회사 다우종합건설      18
주식회사 일성건설        18
대영건설 주식회사        18
대일종합건설 주식회사      18
제이티건설(주)         18
(주)삼구건설          18
태창건설합자회사         18
주식회사 조영건설        18
제이케이종합건설 주식회사    18
주식회사 백율          17
Name: 업체명, dtype: int64

시각화 함수화

def visualize_pct_time(data):
    plt.figure(figsize=(10, 6))
    d = data.groupby('year')['사정율'].mean()
    sns.barplot(x=d.index, y=d)
    plt.ylim(d.min() - 0.02, d.max() + 0.02)
    plt.title('평균 사정율 (연도별)', fontsize=18)
    plt.show()
    print(d)
    print('===='*10)
    
    plt.figure(figsize=(10, 6))
    d = data.groupby('quarter')['사정율'].mean()
    sns.barplot(x=d.index, y=d)
    plt.ylim(d.min() - 0.02, d.max() + 0.02)
    plt.title('평균 사정율 (분기별)', fontsize=18)
    plt.show()
    print(d)
    print('===='*10)
    
    plt.figure(figsize=(10, 6))
    d = data.groupby('month')['사정율'].mean()
    sns.barplot(x=d.index, y=d)
    plt.ylim(d.min() - 0.02, d.max() + 0.02)
    plt.title('평균 사정율 (월별)', fontsize=18)
    plt.show()
    print(d)
    print('===='*10)
    
    plt.figure(figsize=(10, 6))
    d = data.groupby('day')['사정율'].mean()
    sns.barplot(x=d.index, y=d)
    plt.ylim(d.min() - 0.02, d.max() + 0.02)
    plt.title('평균 사정율 (일자별)', fontsize=18)
    plt.show()
    print(d)
    print('===='*10)
    
    plt.figure(figsize=(10, 6))
    d = data.groupby('dayofweek')['사정율'].mean()
    sns.barplot(x=d.index, y=d)
    plt.ylim(d.min() - 0.02, d.max() + 0.02)
    plt.title('평균 사정율 (요일별)', fontsize=18)
    plt.show()
    print(d)
    print('===='*10)
    
    plt.figure(figsize=(10, 6))
    d = data.groupby('hour')['사정율'].mean()
    sns.barplot(x=d.index, y=d)
    plt.ylim(d.min() - 0.02, d.max() + 0.02)
    plt.title('평균 사정율 (시간별)', fontsize=18)
    plt.show()
    print(d)
    print('===='*10)
    
    plt.figure(figsize=(10, 6))
    d = data.groupby('minute')['사정율'].mean()
    sns.barplot(x=d.index, y=d)
    plt.ylim(d.min() - 0.02, d.max() + 0.02)
    plt.title('평균 사정율 (분별)', fontsize=18)
    plt.show()
    print(d)
    print('===='*10)
    
    plt.figure(figsize=(10, 6))
    d = data.groupby('weekofyear')['사정율'].mean()
    sns.barplot(x=d.index, y=d)
    plt.ylim(d.min() - 0.02, d.max() + 0.02)
    plt.title('평균 사정율 (주차별)', fontsize=18)
    plt.show()
    print(d)
    print('===='*10)
%%javascript
IPython.OutputArea.auto_scroll_threshold = 9999;

top 50개 업체 분석

df['업체명'].value_counts().head(50)
태양종합건설 주식회사      28
신우건설 주식회사        28
남광건설 주식회사        27
신영종합건설(주)        25
주식회사 우리종합건설      25
일진건설 주식회사        25
동우건설 주식회사        24
삼진건설(주)          24
주식회사 혜성종합건설      23
보광종합건설 주식회사      23
우일종합건설(주)        22
효창건설(주)          22
청암종합건설 주식회사      22
영진건설(주)          22
대연종합건설 주식회사      22
거성종합건설(주)        21
세움종합건설 주식회사      21
세원종합건설 주식회사      21
우진종합건설(주)        20
두마종합건설주식회사       20
주식회사 부림종합건설      20
주식회사 덕성건설        20
우림종합건설(주)        20
서림종합건설 주식회사      20
유창건설 주식회사        20
(주)동일종합건설        20
대림종합건설 주식회사      20
예림종합건설(주)        20
청진건설주식회사         20
주식회사 태화건설        19
대창건설(주)          19
성원건설(주)          19
승원종합건설(주)        19
제이와이건설 주식회사      19
금종종합건설 주식회사      19
(주)거성종합건설        19
주식회사 에스엠건설       19
성인종합건설 주식회사      19
대호종합건설(주)        19
대원건설(주)          18
주식회사 다우종합건설      18
주식회사 일성건설        18
대영건설 주식회사        18
대일종합건설 주식회사      18
제이티건설(주)         18
(주)삼구건설          18
태창건설합자회사         18
주식회사 조영건설        18
제이케이종합건설 주식회사    18
주식회사 백율          17
Name: 업체명, dtype: int64
company = list(df['업체명'].value_counts().head(50).keys())
company[:5]
['태양종합건설 주식회사', '신우건설 주식회사', '남광건설 주식회사', '신영종합건설(주)', '주식회사 우리종합건설']
df.loc[df['업체명'].isin(company)].head()
업체명 기초금액 예가 사정율 year month day hour minute dayofweek ... 부산 제주 대구 인천 대전 광주 울산 전국 세종 근접율
0 제이티건설(주) 166200000.0 99.407501 99.456497 2020 3 13 16 0 4 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.048996
7 (주)동일종합건설 131254000.0 100.563698 100.712997 2020 3 13 15 0 4 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.149300
19 대림종합건설 주식회사 152384992.0 100.215202 100.254097 2020 3 13 11 0 4 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.038895
20 (주)동일종합건설 140848000.0 99.618202 99.675301 2020 3 12 17 0 3 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.057098
48 우림종합건설(주) 99250000.0 99.945999 99.966797 2020 3 11 14 0 2 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.020798

5 rows × 32 columns

df.loc[df['업체명'].isin(company)].shape
(1032, 32)

상위 50개 업체에 대한 사정율 분석

visualize_pct_time(df.loc[df['업체명'].isin(company)])
year
2018     99.614723
2019     99.650284
2020    100.047600
Name: 사정율, dtype: float32
========================================
quarter
1    99.524040
2    99.793167
3    99.495934
4    99.726967
Name: 사정율, dtype: float32
========================================
month
1     100.058823
2      98.448105
3     100.153893
4      99.295822
5      99.994537
6     100.129677
7      99.948189
8     100.118813
9      96.680954
10     99.025192
11    100.005043
12     99.979912
Name: 사정율, dtype: float32
========================================
day
1     101.271019
2     100.231270
3      97.381348
4      99.821388
5      99.954758
6      99.833641
7     100.318314
8     100.020660
9      99.737701
10     99.918396
11     98.235985
12    100.185211
13    100.478363
14     99.994896
15     99.834633
16    100.165291
17    100.220901
18     99.927635
19     99.911423
20    100.103706
21     97.082840
22    100.683266
23    100.495117
24     99.625702
25     99.886864
26    100.223473
27     99.914177
28     99.872894
29     99.967613
30     96.702568
31     99.965637
Name: 사정율, dtype: float32
========================================
dayofweek
0     99.562622
1     99.574066
2     99.974068
3     99.014107
4    100.241432
Name: 사정율, dtype: float32
========================================
hour
10     50.258099
11    100.028793
12     98.877197
13     99.950279
14     97.659386
15     99.175568
16     99.841110
17    100.710976
Name: 사정율, dtype: float32
========================================
minute
0      99.656639
2      99.977829
5      99.748566
7     100.364998
8     100.026352
10     99.964233
19    100.351303
20    100.712151
25     99.957802
30     99.941765
40    100.157654
50    100.236298
Name: 사정율, dtype: float32
========================================
weekofyear
1      99.661919
2     100.014099
3     100.747063
4      99.940147
5     100.009071
6      99.955833
7      99.823471
8      95.927040
9      99.859200
10    100.142670
11    100.001343
12    100.518356
13     99.893852
14    100.370247
15     99.850792
16    100.627945
17     99.982849
18     95.889160
19     99.727051
20    100.228340
21    100.135078
22    100.518684
23     99.685753
24    100.148605
25     99.961456
26     99.614449
27     99.932823
28    100.017334
29     99.884766
30     99.870316
31    100.464653
32     99.721504
33     99.657547
34    100.685196
35     99.553894
36     75.060875
37    100.103607
38    100.163940
39    100.151520
40     99.986008
41     96.657692
42     99.729042
43    100.439697
44     99.698433
45     99.980774
46    100.067871
47    100.138771
48     99.932625
49     99.713791
50     99.992912
51    100.159637
52    100.053780
Name: 사정율, dtype: float32
========================================
df.head()
업체명 기초금액 예가 사정율 year month day hour minute dayofweek ... 부산 제주 대구 인천 대전 광주 울산 전국 세종 근접율
0 제이티건설(주) 166200000.0 99.407501 99.456497 2020 3 13 16 0 4 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.048996
1 통일종합건설 주식회사 49000000.0 99.273003 99.513802 2020 3 13 16 0 4 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.240799
2 성문건설 주식회사 35000000.0 99.120697 99.411400 2020 3 13 16 0 4 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.290703
3 (주)덕산종합건설 27000000.0 99.877800 99.988602 2020 3 13 16 0 4 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.110802
4 (유)아라야 64460000.0 99.681801 99.684502 2020 3 13 16 0 4 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.002701

5 rows × 32 columns

9. 예측

  • 사정율이 중요한 feature라 판단
  • 따라서, 사정율이 0인 컬럼은 제외합니다
train_data = df.loc[df['사정율'] > 85]
train_data.shape
(31595, 32)

업체명은 drop 합니다

train_data = train_data.drop('업체명', 1)

지역은 서울, 전국을 포함하는 지표만 추립니다

train_data = train_data.loc[(train_data['서울'] == 1) | (train_data['전국'] == 1)]
area_drop_cols = ['전남', '경북', '경기', '경남', '강원', '전북', '충남', '충북', '부산', '제주', '대구',
       '인천', '대전', '광주', '울산', '세종']
train_data = train_data.drop(area_drop_cols, axis=1)

예가, 사정율, 근접율은 사후에 나오는 지표 이므로 drop 합니다

train_feature = train_data.drop(['예가', '사정율', '근접율'], 1)
train_label = train_data['예가']

최종 Feature

train_feature.head()
기초금액 year month day hour minute dayofweek weekofyear dayofyear quarter 서울 전국
167 4.674210e+08 2020 3 6 15 0 4 10 66 1 1.0 0.0
219 1.154583e+09 2020 3 6 11 0 4 10 66 1 1.0 0.0
235 6.420660e+08 2020 3 5 15 0 3 10 65 1 1.0 0.0
275 1.330120e+09 2020 3 5 11 0 3 10 65 1 1.0 0.0
305 9.960500e+08 2020 3 4 15 0 2 10 64 1 1.0 0.0

모델링

from sklearn.model_selection import KFold
from xgboost import XGBRegressor
from lightgbm import LGBMRegressor
SEED = 123
# 코드 비공개 처리
[0]	validation_0-mae:97.43237	validation_1-mae:97.48483
Multiple eval metrics have been passed: 'validation_1-mae' will be used for early stopping.

Will train until validation_1-mae hasn't improved in 30 rounds.
[200]	validation_0-mae:1.72026	validation_1-mae:1.77291
[400]	validation_0-mae:0.53526	validation_1-mae:0.61973
Stopping. Best iteration:
[390]	validation_0-mae:0.53786	validation_1-mae:0.61945

[0]	validation_0-mae:97.45020	validation_1-mae:97.41235
Multiple eval metrics have been passed: 'validation_1-mae' will be used for early stopping.

Will train until validation_1-mae hasn't improved in 30 rounds.
[200]	validation_0-mae:1.72187	validation_1-mae:1.68547
Stopping. Best iteration:
[349]	validation_0-mae:0.55724	validation_1-mae:0.57244

[0]	validation_0-mae:97.43646	validation_1-mae:97.46812
Multiple eval metrics have been passed: 'validation_1-mae' will be used for early stopping.

Will train until validation_1-mae hasn't improved in 30 rounds.
[200]	validation_0-mae:1.72254	validation_1-mae:1.75116
[400]	validation_0-mae:0.54700	validation_1-mae:0.58112
Stopping. Best iteration:
[420]	validation_0-mae:0.54295	validation_1-mae:0.58088

[0]	validation_0-mae:97.44897	validation_1-mae:97.41762
Multiple eval metrics have been passed: 'validation_1-mae' will be used for early stopping.

Will train until validation_1-mae hasn't improved in 30 rounds.
[200]	validation_0-mae:1.72082	validation_1-mae:1.69088
Stopping. Best iteration:
[369]	validation_0-mae:0.54636	validation_1-mae:0.59549

[0]	validation_0-mae:97.44544	validation_1-mae:97.43135
Multiple eval metrics have been passed: 'validation_1-mae' will be used for early stopping.

Will train until validation_1-mae hasn't improved in 30 rounds.
[200]	validation_0-mae:1.72041	validation_1-mae:1.70751
[400]	validation_0-mae:0.54088	validation_1-mae:0.59959
Stopping. Best iteration:
[378]	validation_0-mae:0.54601	validation_1-mae:0.59915

avg score: 0.593481
pd.DataFrame(list(zip(train_feature.columns, xgb_model.feature_importances_))).sort_values(by=1, ascending=False)
0 1
9 quarter 0.212947
0 기초금액 0.090076
8 dayofyear 0.089709
11 전국 0.078249
7 weekofyear 0.075134
5 minute 0.074531
6 dayofweek 0.070992
3 day 0.070871
10 서울 0.069562
4 hour 0.059260
1 year 0.055194
2 month 0.053476
def train_xgb(train_features, train_labels, test_features, num_split=3):
    # 코드 비공개 처리
    return preds
def train_lgbm(train_feature, train_label, test_feature, num_split=5):
    # 코드 비공개 처리
    return preds
def train_xgb(train_feature, train_label, test_feature, num_split=3):
    # 코드 비공개 처리
    return preds
from sklearn.model_selection import train_test_split

x_train, x_valid, y_train, y_valid = train_test_split(train_feature, train_label, random_state=321)
x_train.shape, x_valid.shape
((1790, 12), (597, 12))
lgbm_pred = train_lgbm(x_train, y_train, x_valid)
xgb_pred = train_xgb(x_train, y_train, x_valid)
Training until validation scores don't improve for 30 rounds
Early stopping, best iteration is:
[1]	training's l1: 0.596837	training's l2: 0.544079	valid_1's l1: 0.599506	valid_1's l2: 0.56646
Training until validation scores don't improve for 30 rounds
Early stopping, best iteration is:
[9]	training's l1: 0.608888	training's l2: 0.56613	valid_1's l1: 0.541242	valid_1's l2: 0.460266
Training until validation scores don't improve for 30 rounds
Early stopping, best iteration is:
[1]	training's l1: 0.590583	training's l2: 0.540623	valid_1's l1: 0.624393	valid_1's l2: 0.580429
Training until validation scores don't improve for 30 rounds
Early stopping, best iteration is:
[1]	training's l1: 0.593366	training's l2: 0.541869	valid_1's l1: 0.613335	valid_1's l2: 0.57537
Training until validation scores don't improve for 30 rounds
Early stopping, best iteration is:
[19]	training's l1: 0.58905	training's l2: 0.536167	valid_1's l1: 0.609964	valid_1's l2: 0.563332
avg score: 0.5976880338001499
[0]	validation_0-mae:97.44695	validation_1-mae:97.39553
Multiple eval metrics have been passed: 'validation_1-mae' will be used for early stopping.

Will train until validation_1-mae hasn't improved in 20 rounds.
[200]	validation_0-mae:1.72761	validation_1-mae:1.67668
Stopping. Best iteration:
[366]	validation_0-mae:0.54420	validation_1-mae:0.58611

[0]	validation_0-mae:97.43077	validation_1-mae:97.42883
Multiple eval metrics have been passed: 'validation_1-mae' will be used for early stopping.

Will train until validation_1-mae hasn't improved in 20 rounds.
[200]	validation_0-mae:1.72430	validation_1-mae:1.72252
Stopping. Best iteration:
[349]	validation_0-mae:0.55145	validation_1-mae:0.61090

[0]	validation_0-mae:97.41282	validation_1-mae:97.46614
Multiple eval metrics have been passed: 'validation_1-mae' will be used for early stopping.

Will train until validation_1-mae hasn't improved in 20 rounds.
[200]	validation_0-mae:1.72471	validation_1-mae:1.77892
[400]	validation_0-mae:0.52633	validation_1-mae:0.62074
Stopping. Best iteration:
[400]	validation_0-mae:0.52633	validation_1-mae:0.62074

avg score: 0.6059176666666666
pred = lgbm_pred * 0.6 + xgb_pred * 0.4
abs(pred - y_valid).mean()
0.5554264117899838

검증 (시뮬레이션)

validation_df = df.iloc[y_valid.index][['예가', '사정율']].copy()
validation_df.head()
예가 사정율
14548 98.969002 98.977898
5612 99.151497 99.167297
16649 100.463997 100.474098
26280 99.882004 99.883202
17308 99.960197 99.964798
df.iloc[y_valid.index]['사정율'].head()
14548     98.977898
5612      99.167297
16649    100.474098
26280     99.883202
17308     99.964798
Name: 사정율, dtype: float32
validation_df['pred'] = pred
validation_df.head()
예가 사정율 pred
14548 98.969002 98.977898 99.907663
5612 99.151497 99.167297 99.847656
16649 100.463997 100.474098 99.921612
26280 99.882004 99.883202 99.940660
17308 99.960197 99.964798 99.905172

괴리율 = 예측값 - 예가 (0보다 작으면 무조건 탈락)

validation_df['괴리율'] = validation_df['pred'] - validation_df['예가']
validation_df['선정여부'] = (validation_df['pred'] < validation_df['사정율']) & (validation_df['pred'] > validation_df['예가'])
validation_df.loc[validation_df['괴리율'] >= 0].sort_values(by='괴리율').head()
예가 사정율 pred 괴리율 선정여부
7468 99.897202 99.933403 99.897466 0.000264 True
6025 99.857002 99.860001 99.857646 0.000643 True
21792 99.869003 99.874496 99.873144 0.004141 True
17479 99.917999 99.919998 99.924551 0.006552 False
26677 99.924004 99.925003 99.938099 0.014095 False

선정된 지표 확인

validation_df.loc[validation_df['선정여부'] ==True]
예가 사정율 pred 괴리율 선정여부
22744 99.438499 100.062599 99.869264 0.430764 True
10751 97.873703 100.247398 99.918239 2.044536 True
26726 99.441704 101.254898 99.859676 0.417972 True
6025 99.857002 99.860001 99.857646 0.000643 True
23983 99.671700 105.500000 99.884492 0.212792 True
21792 99.869003 99.874496 99.873144 0.004141 True
30818 99.890999 99.925003 99.905421 0.014422 True
7468 99.897202 99.933403 99.897466 0.000264 True
21491 99.849701 99.899803 99.897837 0.048136 True
idx = validation_df.loc[validation_df['선정여부'] ==True].index

선정된 지표 (전체데이터)

df.iloc[idx]
업체명 기초금액 예가 사정율 year month day hour minute dayofweek ... 부산 제주 대구 인천 대전 광주 울산 전국 세종 근접율
22744 (주)아랑존디 4.840000e+07 99.438499 100.062599 2018 10 19 11 0 4 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0 0.0 0.624100
10751 (주)지투원 1.732107e+09 97.873703 100.247398 2019 7 9 11 0 1 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 2.373695
26726 계룡건설산업 주식회사 2.322301e+10 99.441704 101.254898 2018 6 26 11 0 1 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0 0.0 1.813194
6025 세한종합건설 주식회사 1.335007e+10 99.857002 99.860001 2019 11 14 11 0 3 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0 0.0 0.002998
23983 중미건설합자회사 1.501838e+10 99.671700 105.500000 2018 9 10 17 0 0 ... 0.0 0.0 0.0 0.0 0.0 1.0 0.0 1.0 0.0 5.828300
21792 새움綜合建設 株式會社 1.856688e+09 99.869003 99.874496 2018 11 8 11 0 3 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.005493
30818 (주)이공 9.640700e+08 99.890999 99.925003 2018 4 6 11 0 4 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.034004
7468 주식회사 예진건설 1.083887e+10 99.897202 99.933403 2019 10 22 11 0 1 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0 0.0 0.036201
21491 갑인종합건설주식회사 3.914170e+09 99.849701 99.899803 2018 11 14 11 0 2 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0 0.0 0.050102

9 rows × 32 columns

댓글남기기