Project_DACON_상점 신용카드 매출 예측 경진대회



- Portfolio for Time-Series

  • 개인적인 시계열 분석 포트폴리오 작성을 위하여, 아래의 DACON 대회 자료를 활용하였습니다. (현재종료 2019년 대회)
  • 대회유형: 시계열 예측
  • 대회목적: 핀테크 기업인 ‘FUNDA(펀다)’는 상환 기간의 매출을 예측하여 신용 점수가 낮거나 담보를 가지지 못하는 우수 상점들에 금융 기회를 제공하려 합니다. 이번 대회에서는 2년 전 부터 2019년 2월 28일까지의 카드 거래 데이터를 이용해 2019-03-01부터 2019-05-31까지의 각 상점별 3개월 총 매출을 예측하는 것입니다.
  • 데이터제공기간: 2016-06-01 ~ 2019-2-28 (1002 days)
  • 데이터예측기간: 2019-03-01 ~ 2019-05-31 (92 days)
  • 데이터사이즈: 6,556,613 entries , 9 columns, memory 450MB
    Link: https://dacon.io/competitions/official/140472/overview/
  • Data Description
    store_id : 상점의 고유 아이디
    card_id : 사용한 카드의 고유 아이디
    card_company : 비식별화된 카드 회사
    trasacted_date : 거래 날짜
    transacted_time : 거래 시간( 시:분 )
    installment_term : 할부 개월 수( 포인트 사용 시 (60개월 + 실제할부개월)을 할부개월수에 기재한다. )
    region : 상점의 지역
    type_of_business : 상점의 업종
    amount : 거래액(단위는 원이 아닙니다)

- Project Summary

  1. 프로젝트 진행 일정
    2020-10-07 ~ 2020-10-16 (9 days)
  1. 개발환경
    OS: Window
    IDE: Visual Studio Code, Jupyter Notebook
    VCS: Git, SourceTree
  1. 프로젝트 최종 결과 (9등, 상위권)
    Model MAE(Mean Absolute Error,
    평균절대오차)
    Public Board Rank
    Exponential_Moving Average(5) 765,887 9등(최종)
    Exponential_Moving Average(3) 784,219 18등
    Simple_Moving_Average(3) 810,333 25등
    ARIMA_n_LGBM 1,768,874 93등
    ARIMA 1,062,635 50등
    LGBM 2,481,832 102등

*우측 CATALOGUE 에서 Conclusion 링크를 클릭하시면 프로젝트 결과에 관한 설명 및 요약 리포트를 확인하실 수 있습니다.

  1. 프로젝트 목차

    -1. Portfolio for Time-Series
    -2. Project Summary
    -3. Data_Load
    -4. EDA_Part_1
    -5. EDA_Part_2
    -6. Data Preprocessing
    -7. LGBM Modeling
    -8. Modeling_MA&EPMA&ARIMA
    -9. Moving Average
    -10. Exponential Moving Average
    -11. ARIMA
    -12. Conclusion

Data_Load

1
2
3
4
5
6
7
8
9
10
11
12
13
import numpy as np
import pandas as pd
from tqdm.autonotebook import tqdm
import datetime
from datetime import date
from datetime import timedelta
from lightgbm import LGBMRegressor
from sklearn.preprocessing import LabelEncoder
import plotly_express as px
import matplotlib.pylab as plt
plt.rcParams['font.family'] = 'Malgun Gothic'
import seaborn as sns
from statsmodels.tsa.seasonal import seasonal_decompose
C:\ProgramData\Anaconda3\lib\site-packages\ipykernel_launcher.py:3: TqdmExperimentalWarning: Using `tqdm.autonotebook.tqdm` in notebook mode. Use `tqdm.tqdm` instead to force console mode (e.g. in jupyter console)
  This is separate from the ipykernel package so we can avoid doing imports until
1
2
# raw data load 
sales = pd.read_csv('C:\Archaon\projects\Card_Sales\Dataset\copy_train.csv')

EDA_Part_1

1
2
# 1. raw data
sales.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 6556613 entries, 0 to 6556612
Data columns (total 9 columns):
 #   Column            Dtype  
---  ------            -----  
 0   store_id          int64  
 1   card_id           int64  
 2   card_company      object 
 3   transacted_date   object 
 4   transacted_time   object 
 5   installment_term  int64  
 6   region            object 
 7   type_of_business  object 
 8   amount            float64
dtypes: float64(1), int64(3), object(5)
memory usage: 450.2+ MB
1
2
# 1.1 missing values 
sales.isnull().sum()
store_id                  0
card_id                   0
card_company              0
transacted_date           0
transacted_time           0
installment_term          0
region              2042766
type_of_business    3952609
amount                    0
dtype: int64
1
2
3
# Before Downcasting 
sales_bd = np.round(sales.memory_usage().sum()/(1024*1024),1)
sales_bd #450
450.2
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
# Memory Downcast
# 데이터 메모리 축소를 위한 downcast 함수 생성 및 적용

def downcast(df):
cols = df.dtypes.index.tolist()
types = df.dtypes.values.tolist()
for i,t in enumerate(types):
if 'int' in str(t):
if df[cols[i]].min() > np.iinfo(np.int8).min and df[cols[i]].max() < np.iinfo(np.int8).max:
df[cols[i]] = df[cols[i]].astype(np.int8)
elif df[cols[i]].min() > np.iinfo(np.int16).min and df[cols[i]].max() < np.iinfo(np.int16).max:
df[cols[i]] = df[cols[i]].astype(np.int16)
elif df[cols[i]].min() > np.iinfo(np.int32).min and df[cols[i]].max() < np.iinfo(np.int32).max:
df[cols[i]] = df[cols[i]].astype(np.int32)
else:
df[cols[i]] = df[cols[i]].astype(np.int64)
elif 'float' in str(t):
if df[cols[i]].min() > np.finfo(np.float16).min and df[cols[i]].max() < np.finfo(np.float16).max:
df[cols[i]] = df[cols[i]].astype(np.float16)
elif df[cols[i]].min() > np.finfo(np.float32).min and df[cols[i]].max() < np.finfo(np.float32).max:
df[cols[i]] = df[cols[i]].astype(np.float32)
else:
df[cols[i]] = df[cols[i]].astype(np.float64)
elif t == np.object:
if cols[i] == 'date':
df[cols[i]] = pd.to_datetime(df[cols[i]], format='%Y-%m-%d')
else:
df[cols[i]] = df[cols[i]].astype('category')
return df
1
2
3
4
5
6
# Downcast Data
sales = downcast(sales)

# After Downcasting
sales_ad = np.round(sales.memory_usage().sum()/(1024*1024),1) #125.2
sales_ad
125.2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import plotly_express as px

dic = {'DataFrame':['sales'],
'Before downcasting':[sales_bd],
'After downcasting':[sales_ad]}

memory = pd.DataFrame(dic)
memory = pd.melt(memory, id_vars='DataFrame', var_name='Status', value_name='Memory (MB)')
memory.sort_values('Memory (MB)',inplace=True)

fig = px.bar(memory, x='DataFrame', y='Memory (MB)', color='Status', barmode='group', text='Memory (MB)')
fig.update_traces(texttemplate='%{text} MB', textposition='outside')
fig.update_layout(template='seaborn', title='Effect of Downcasting')
fig.show()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 1.2 columns
# 1.2.1 ['store_id']
len(sales['store_id'].unique()) #1967
## 1967개의 store_id 가 존재한다.
store_counts = sales['store_id'].value_counts().reset_index()
store_counts = pd.DataFrame(store_counts )
store_counts.columns = ['store_id','counts']

plt.figure(figsize=(12,8))
ax = sns.boxplot(x="counts", data =store_counts)
ax = sns.swarmplot(x="counts", data =store_counts, palette="ocean")
ax.set( Xlabel="결제횟수 by Store ID", ylabel="Box Plot")
ax.set_title("<Figure1>, 상점 Store ID 별 결제횟수 분포")
plt.tight_layout()
plt.show()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 1.2.2 ['card_company]
len(sales)
# = 6,556,613
len(sales['card_id'].unique())
# = 3,950,001
## sales 데이터의 entry 는 card_id를 기준으로 작성되었다.
## 따라서, 1 entry = 1 card transaction 으로 이해할 수 있다.
## 하지만 데이터상의 고유 card_id 의 수는 3,950,001 개로, 전체 거래량인 6,556,613 에 비해서 40% 정도 작다.
## 그러므로 전체 거래량의 40%는 동일한 카드의 중복거래란 것을 확인할 수 있다.


sales.card_company
## Categories (8, object): [a, b, c, d, e, f, g, h]

card_company = sales['card_company'].value_counts()
card_company = pd.DataFrame(card_company)

plt.figure(figsize=(12,8))
ax = sns.barplot(x=card_company.index , y = "card_company", palette="CMRmap_r", data =card_company )
ax.set( Xlabel="Card Company", ylabel="결제횟수 합계")
ax.set_title("<Figure2>, 카드회사별 결제횟수 합계")
plt.tight_layout()
plt.show()

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47

# 1.2.4 ['transacted_date']

## 카드 거래일자는 2016년 6월1일부터, 2019년 2월 28일까지이며, day 기준으로 1002 일이다.
## 본 대회의 경우, 1002일 간의 카드거래 데이터로, 향후 3달후의 상점별 매출액을 예상하는 대회이다.
## 따라서 거래일자는 아래와 같은 구간으로 나눠진다.
## train period : 2016.6.1 ~ 2019.2.28 (1002 days)
## test period : 2019.3.1 ~ 2019.5.31 (91 days)

sales['transacted_date']
# 0 2016-06-01
# 1 2016-06-01
# 2 2016-06-01
# 3 2016-06-01
# 4 2016-06-02
# ...
# 6556608 2019-02-28
# 6556609 2019-02-28
# 6556610 2019-02-28
# 6556611 2019-02-28
# 6556612 2019-02-28

# 1.2.5 ['transacted_time'] & ['installment_term]

## transacted_time 과 installment_term 각각 카드거래 시각과 할부 개월수를 의미한다.
## 이러한 분포는 상점의 사업종류에 따라서 편차가 클 것으로 예상된다.
## 상점 하나(store_id == 1000)를 샘플로 골라서, 분포를 살펴보도록 하겠다.
## 선택된 상점의 경우에, type of business 가 '의복 소매업'이다. 따라서 사람들이 많이 쇼핑하는
## 주말 오후 시간대 14 ~ 18 시 사이에, 카드결제 시간이 몰려 있는 것으로 확인된다.

store_1000 = sales[ sales['store_id'] == 1000 ]
###
len(store_1000)
blank = []
for i in range(0,len(store_1000)):
str1 = int(store_1000['transacted_time'].iloc[i][0:2])
blank.append(str1)
blank
blank = pd.DataFrame(blank, columns=['Count'])
time_count = pd.DataFrame(blank['Count'].value_counts())
###
plt.figure(figsize=(12,8))
ax = sns.barplot(x=time_count.index , y = "Count", data =time_count )
ax.set( Xlabel="카드결제시각", ylabel="카드결제횟수")
ax.set_title("<Figure3>, Store_id = 1000, 카드결제시각 당 카드결제횟수")
plt.tight_layout()
plt.show()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 1.2.6 ['region'] & ['type of business']

## region 과 type of business 의 경우에 결측치가 많다.
## region 의 경우에는 총 데이터의 31%가 결측치이며, business의 경우에 60%가 결측치이다.

null_result = sales.isnull().sum()
null_result = pd.DataFrame(null_result,columns=['count'])
null_result = null_result.reset_index()

plt.figure(figsize=(12,8))
ax = sns.barplot(x="index" , y = "count", data =null_result , palette= "ocean" )
ax.set( Xlabel="Data Columns", ylabel="Null Counts")
ax.set_title("<Figure4>, 결측치확인 ")
plt.tight_layout()
# 1.2.7 ['amount']

## 주어진 데이터의 거래액은 카드결제 당 금액으로 입력되어있다. 하지만 예측이 필요한 3개월의 데이터는
## 상점별 매출이므로, amount 금액 또한 store_id 를 기준으로 groupby 한 금액을 본다.
plt.show()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 1.2.7 ['amount']

## 주어진 데이터의 거래액은 카드결제 당 금액으로 입력되어있다. 하지만 예측이 필요한 3개월의 데이터는
## 상점별 매출이므로, amount 금액 또한 store_id 를 기준으로 groupby 한 금액을 본다.
## 상점별 매출의 경우 피라미드 형태의 매출을 보여주고 있으며, 특정 상점이 이상치에 가까운 매출액을 보여주고 있다.

s1 = sales.groupby('store_id').sum().reset_index()
s1['amount'] = s1['amount'].astype(int)

plt.figure(figsize=(12,8))
ax = sns.boxplot(x="amount", data =s1)
ax = sns.swarmplot(x="amount", data=s1, palette="ocean")

ax.set( Xlabel="", ylabel="Card Transaction Amount")
ax.set_title("<Figure5>, 상점별 카드매출액 분포 ")

plt.tight_layout()
plt.show()


EDA_Part_2

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
# sido 데이터 뽑아내기 

#region & type of business 가 있는 데이터로 그룹 나누기
sales_region = sales[['store_id','region']]
sales_region_null = sales_region.isnull()
region_index_list = sales_region_null[ sales_region_null['region']== 0 ].index #length 2042766 #4513847 False
# Sales 데이터에서 region 값이 있는 데이터들을 찾기
sales1 = sales.iloc[ region_index_list, :]
sales1.type_of_business.isnull().sum()


#결측치행 drop
sales_c = sales.copy()
group1 = sales_c.dropna(subset = ['region', 'type_of_business'] )
group1 #[2318410 rows x 9 columns] # group1 = 35% 전체데이터에서

sido_list = group1['region'].str[0:2]
sido_list = pd.DataFrame(sido_list)
sido_list.columns = ['region_sido']
group1_sido = pd.concat([group1, sido_list] ,axis = 1 )
#### group 구분 4단계
## group1 둘다 있음 group1 = 35% 전체데이터에서
## group2 비지니스만
## group3 지역만
## group4 둘다없음
1
2
3
4
5
6
7
8
9
10
11
12
# Group1 시도별, amount 금액 합계 
ra = group1_sido.groupby('region_sido')['amount'].sum()
ras = ra.reset_index()
ras = ras.sort_values(by=['amount'], axis=0,ascending=False)

plt.figure(figsize=(12,8))
ax = sns.barplot(x='region_sido' , y = "amount", data =ras )
ax.set( Xlabel="시도", ylabel="카드매출금액합계")
ax.set_title("<Figure6>, 시도별 카드매출금액 합계 ")

plt.tight_layout()
plt.show()

1
2
3
4
5
6
7
8
9
10
11
# 시도별, 결제 횟수 합계 
rs = group1_sido['region_sido'].value_counts()
res = rs.reset_index()
res

plt.figure(figsize=(12,8))
ax = sns.barplot(x='index' , y = "region_sido", data =res )
ax.set( Xlabel="카드결제횟수 합계", ylabel="counts")
ax.set_title("<Figure7>, 시도별 카드결제횟수 합계 ")
plt.tight_layout()
plt.show()

1
2
3
4
5
6
7
8
9
10
11
12
# 업종별 결제금액 합계 
tba = group1_sido.groupby('type_of_business')['amount'].sum()
tba = tba.reset_index()
tbas = tba.sort_values(by=['amount'], axis=0,ascending=False)

plt.figure(figsize=(12,8))
ax = sns.barplot(y='type_of_business' , x="amount", data =tbas, order=tbas.sort_values('amount',ascending = False).type_of_business )
ax.set( Xlabel="결제금액합", ylabel="type_of_business", )
ax.set_title("<Figure8>, 업종별 카드결제금액 합계 ")
ax.set_xticklabels(ax.get_xticklabels(), rotation=90)
plt.tight_layout()
plt.show()

1
2
3
## 결제금액 top 10, Business type 
new_tb= tbas[0:9]
new_tb

type_of_business amount
139 한식 음식점업 4.355464e+09
99 의복 소매업 1.954874e+09
101 의약품 도매업 1.568540e+09
105 일반 교과 학원 1.168244e+09
54 두발 미용업 8.859701e+08
108 일식 음식점업 8.170945e+08
46 기타 주점업 6.550956e+08
37 기타 스포츠 교육기관 6.052595e+08
82 안경 및 렌즈 소매업 5.064083e+08
1
2
3
4
5
6
7
8
## 매출 top 10 업종이 전체 매출에서 차지하는 비중 ## 
group1['amount'].sum() #25,657,748,130
tbas['amount'][0:10].sum() #1,071,393,938
#13020321268 / 25657748130 # 상위 10개가 전체 매출의 50 %
tbas['amount'][0:5].sum() #9,933,091,985
#9933091985 / 25657748130 # 상위 5개가 전체 매출의 38 %
tbas['amount'][0:1].sum() #4,355,463,710
#4355463710 / 25657748130 # top 1(한식음식점업)이 전체 매출의 16%
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
30
31
32
33
34
## top3 지역별 매출액 합계 
group1_sido['transacted_date'] = pd.to_datetime(group1_sido['transacted_date'] )

group1_gy = group1_sido[group1_sido['region_sido'] == "경기"]
group1_gy_new = group1_gy.groupby( 'transacted_date' )['amount'].sum()
group1_gy_new = group1_gy_new.reset_index()

group1_se = group1_sido[group1_sido['region_sido'] == "서울"]
group1_se_new = group1_se.groupby( 'transacted_date' )['amount'].sum()
group1_se_new = group1_se_new.reset_index()

group1_pu = group1_sido[group1_sido['region_sido'] == "부산"]
group1_pu_new = group1_pu.groupby( 'transacted_date' )['amount'].sum()
group1_pu_new = group1_pu_new.reset_index()

fig, ax = plt.subplots(nrows=3, figsize = (18, 12))
#f,axes = plt.subplots(3,1, figsize = (18,12), sharex=True)
sns.lineplot(y="amount" , x="transacted_date", data =group1_gy_new, ax= ax[0])
sns.lineplot(y="amount" , x="transacted_date", data =group1_se_new, ax= ax[1])
sns.lineplot(y="amount" , x="transacted_date", data =group1_pu_new, ax= ax[2])


ax[0].set( Xlabel="", ylabel="매출액", )
ax[0].set_title("<Figure9>, 경기지역 매출액 합계 ")

ax[1].set( Xlabel="", ylabel="매출액", )
ax[1].set_title("<Figure10>, 서울지역 매출액 합계 ")

ax[2].set( Xlabel="", ylabel="매출액", )
ax[2].set_title("<Figure11>, 부산지역 매출액 합계 ")


plt.show()

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
30
31
32
33
34
## top3 업종별 매출액 합계 
group1_sido['transacted_date'] = pd.to_datetime(group1_sido['transacted_date'] )

group1_gyr = group1_sido[group1_sido['type_of_business'] == "한식 음식점업"]
group1_gyr_new = group1_gyr.groupby( 'transacted_date' )['amount'].sum()
group1_gyr_new = group1_gyr_new.reset_index()

group1_ser = group1_sido[group1_sido['type_of_business'] == "의복 소매업"]
group1_ser_new = group1_ser.groupby( 'transacted_date' )['amount'].sum()
group1_ser_new = group1_ser_new.reset_index()

group1_pur = group1_sido[group1_sido['type_of_business'] == "의약품 도매업"]
group1_pur_new = group1_pur.groupby( 'transacted_date' )['amount'].sum()
group1_pur_new = group1_pur_new.reset_index()

fig, ax = plt.subplots(nrows=3, figsize = (18, 12))
#f,axes = plt.subplots(3,1, figsize = (18,12), sharex=True)
sns.lineplot(y="amount" , x="transacted_date", data =group1_gyr_new, ax= ax[0])
sns.lineplot(y="amount" , x="transacted_date", data =group1_ser_new, ax= ax[1])
sns.lineplot(y="amount" , x="transacted_date", data =group1_pur_new, ax= ax[2])


ax[0].set( Xlabel="", ylabel="매출액", )
ax[0].set_title("<Figure11>, 한식 음식점업 매출액 합계 ")

ax[1].set( Xlabel="", ylabel="매출액", )
ax[1].set_title("<Figure12>, 의복 소매업 매출액 합계 ")

ax[2].set( Xlabel="", ylabel="매출액", )
ax[2].set_title("<Figure13>, 의약품 도매업 매출액 합계 ")


plt.show()

1
2
3
4
### top 매출업종 한식음식점업 결제금액 합계  
group1_group = group1_sido.groupby(['type_of_business','transacted_date']).sum().reset_index()
group1_group['transacted_date'] = pd.to_datetime(group1_group['transacted_date'])
hansik = group1_group[ group1_group['type_of_business'] =="한식 음식점업" ]
1
2
3
4
5
plt.figure(figsize=(18,10))
ax = sns.lineplot(y='amount' , x="transacted_date", data =hansik)
ax.set( Xlabel="", ylabel="결제금액", )
ax.set_title("<Figure14>, 한식음식점업 결제금액 합계")
plt.show()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
### STL 분해 
from statsmodels.tsa.seasonal import seasonal_decompose
import warnings
warnings.filterwarnings(action='ignore')

# 매출 1위 업종인 한식업종에 대한 STL 분해
seasonal_hansik = hansik[['transacted_date','amount']].set_index('transacted_date')
ts = seasonal_hansik.amount
result = seasonal_decompose(x= ts , model='additive', freq=12)

plt.rcParams["figure.figsize"] = (16,12)
ax = result.plot()

plt.show()

1
2
3
4
5
6
7
8
9
10
11
12
### ARIMA 분석을 위한 ACF, PACF 그래프  
import matplotlib.pyplot as plt
from statsmodels.graphics.tsaplots import plot_acf, plot_pacf

# 한식업종 매출에 대한 ACF, PACF 그래프
hansik_ap = hansik[['transacted_date','amount']]
hansik_ap = hansik_ap.set_index('transacted_date')

plot_acf(hansik_ap)
plot_pacf(hansik_ap)
plt.show()

Data Preprocessing

LGBM Modeling

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
# LGBM 모델링을 위한 데이터 전처리 

# 전체 데이터를 region 과 business 열에 결측치가 유무로 두 가지의 그룹으로 구분하였다.
# group1(35% of all data) : region 과 business 값들에 결측치 없음
# group2(65% of all data) : region 과 business 에 결측치가 존재함
#
# 보통의 경우에 위와 같이 결측치의 비중이 10~20%를 초과하면은 해당 칼럼들을 사용하지 않지만,
# 해당 데이터의 경우에, region과 business 를 제외하면은 효과적으로 데이터를 구분해줄 category 변수가 전혀 존재하지 않는다.
# 따라서 tree 기반 모델링을 위해서 data 를 두 그룹으로 나누고, 한쪽 그룹에는 tree 기반 모델링을 하고, 다른 한쪽 그룹에는
# non-tree 기반한 모델링을 하여서 그러한 구분이 결과값에 어떠한 영향을 끼쳤는지와 그 이유를 탐구해 볼 예정이다.



# LGBM 모데링을 위한 group1 데이터셋 전처리
def group1_make(data):
### group1 dataset making
# region과 business에서 결측치가 있는 행들을 제외
group1 = data.dropna(subset = ['region', 'type_of_business'] )
# region 에서, 시/도 추출
sido_list = group1['region'].str[0:2]
sido_list = pd.DataFrame(sido_list)
sido_list.columns = ['region_sido']

group1_sido = pd.concat([group1, sido_list] ,axis = 1 )
group1_sido = group1_sido.drop('region',axis=1)
return group1_sido

group1_dataset = group1_make(sales)


def group1_make_2nd(data):
### group1 transform datetime & groupby year and month

dataset = pd.DataFrame([])
for i in tqdm(data.store_id.unique()):
datas = data[ data['store_id'] == i ]

id_region_sido = datas['region_sido'].unique()[0]
id_type_of_business = datas['type_of_business'].unique()[0]
# 불필요한 column 들 drop
datas = datas.drop(['card_id', 'card_company', 'transacted_time', 'installment_term'], axis=1)
# 날짜 데이터 datetime 변환
datas['transacted_date'] = pd.to_datetime( datas['transacted_date'])
# month 와 year 생성
datas['month'] = datas['transacted_date'].dt.month
datas['year'] = datas['transacted_date'].dt.year

datas_grouped = datas.groupby( ['store_id' , 'year', 'month']).sum().reset_index()

datas_grouped['sido'] = id_region_sido
datas_grouped['business'] = id_type_of_business


dataset = pd.concat( [dataset, datas_grouped], axis=0)
dataset = dataset.reset_index()
dataset = dataset.drop(['index'], axis=1 )
return dataset

group1 = group1_make_2nd(group1_dataset)

Wall time: 0 ns
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

def make_lags(data):
### Feature Engineering : lag & rolling mean
blank = pd.DataFrame([])
for i in tqdm(data.store_id.unique()):
store = data[ data.store_id == i]
store = store.reset_index()
store = store.drop(['index'],axis=1)

# lag 1,2,3,6
lags = [1,2,3,6]
for lag in lags:
store['amount_lag_' + str(lag)] =store.groupby( ['store_id', 'year','month','sido','business'], as_index=False )['amount'].sum().shift(lag).amount

# rolling mean 3,6
windows = [3,6]
for window in windows:
store['rolling_mean_' + str(window)] =store.amount.rolling(window=window).mean()

blank = pd.concat([blank,store], axis=0)
blank = blank.reset_index()
blank = blank.drop( ['index'], axis=1)

return blank

group1_fin = make_lags(group1)

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
30
31
32
33
34
35
36
37
38

def concat_test_set(data):
### MeanEncoding & Test Dataset

# Mean Encoding
# 각각 지역평균
data['sido_avg'] = data.groupby('sido')['amount'].transform('mean')
# 각각 업종평균
data['business_avg'] = data.groupby('business')['amount'].transform('mean')
# 각각 지역 x 업종 평균
data['sido_n_business_avg'] = data.groupby(['sido', 'business'])['amount'].transform('mean')

# Test 구간인, 2019년 3월4월5일 치, 빈 데이터셋 만들기
for i in tqdm(data.store_id.unique()):

X_test_blank = pd.DataFrame( [[0 for i in range(15)] for i in range(3)], columns=['store_id', 'year', 'month', 'amount', 'sido', 'business',
'amount_lag_1', 'amount_lag_2', 'amount_lag_3', 'amount_lag_6',
'rolling_mean_3', 'rolling_mean_6', 'sido_avg', 'business_avg',
'sido_n_business_avg'] )
store = data[data.store_id == i]

X_test_blank['store_id'] = i
X_test_blank['sido'] = store.sido.unique()[0]
X_test_blank['business'] = store.business.unique()[0]

X_test_blank['year'] = 2019
X_test_blank['month'] = [3,4,5]

X_test_blank['sido_avg'] = store.sido_avg.unique()[0]
X_test_blank['business_avg'] = store.business_avg.unique()[0]
X_test_blank['sido_n_business_avg'] = store.sido_n_business_avg.unique()[0]


data = pd.concat( [data, X_test_blank], axis=0)
new_data = data
return new_data

group1_fin_testadd = concat_test_set(group1_fin)
1
2
3
4
5
6
7
8
9
10
cos = group1_fin_testadd
cos = cos.reset_index()
cos = cos.drop('index', axis=1)

cos.columns
# ['store_id', 'year', 'month', 'amount', 'sido', 'business',
# 'amount_lag_1', 'amount_lag_2', 'amount_lag_3', 'amount_lag_6',
# 'rolling_mean_3', 'rolling_mean_6', 'sido_avg', 'business_avg',
# 'sido_n_business_avg']

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
### Data Split

# test #2106 #2019년 3,4,5월
cos1_test = cos[ (cos['year'] == 2019) & ((cos['month'] == 3) | (cos['month'] == 4) | (cos['month'] == 5)) ]
# valid #1366 #2019년 1,2월
cos2_valid = cos[ (cos['year'] == 2019) & ((cos['month'] == 1) | (cos['month'] == 2)) ]
# train #20356 #2016년 ~ 2018년
cos3_train = cos[ (cos['year'] == 2018)| (cos['year'] == 2017) | (cos['year'] == 2016) ]


# Data split
X_train = cos3_train.drop(['sido', 'business','amount'], axis=1)
y_train = cos3_train['amount']

X_valid = cos2_valid.drop(['sido', 'business','amount'], axis=1)
y_valid = cos2_valid['amount']

X_test = cos1_test.drop(['sido', 'business','amount'], axis=1)
y_test = cos1_test['amount']

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# Train and validate
model = LGBMRegressor(
n_estimators=1000,
learning_rate=0.01,
subsample=0.8,
colsample_bytree=0.8,
max_depth=8,
num_leaves=50,
min_child_weight=300)

model.fit(X_train, y_train, eval_set=[(X_train,y_train),(X_valid,y_valid)], eval_metric='mae', verbose=20, early_stopping_rounds=200)

eval_preds = model.predict(X_test)

X_test_result = X_test.copy()
X_test_result['pred'] = eval_preds

### LGBM 결과치
group1_submission = X_test_result.groupby('store_id')['pred'].sum()
group1_submission = group1_submission.reset_index()
group1_submission.columns = ['store_id','amount']
group1_submission.to_csv("submission_LGBM.csv", mode='w')

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
30
# LGBM - Grid Search

from sklearn.metrics import mean_squared_error
from sklearn.metrics import r2_score, mean_squared_error, make_scorer
RMSE = mean_squared_error(y_test, pred)**0.5

import sklearn.metrics
sklearn.metrics.SCORERS.keys()

# Grid Search
from sklearn.model_selection import GridSearchCV

gridParams = {
'learning_rate': [0.01, 0.1, 0.2 ],
'n_estimators': [8,32,64,100],
'num_leaves': [6,12,32] ,
'min_data_in_leaf' : [8, 20, 36],
'max_bin':[255, 510],
'random_state' : [500],
'colsample_bytree' : [0.64, 0.65, 0.66],
'subsample' : [0.7,0.75],
'reg_alpha' : [1,1.2],
'reg_lambda' : [1,1.2,1.4],
}

grid_cv = GridSearchCV(model, param_grid = gridParams, n_jobs=-1, scoring='neg_mean_absolute_error')

grid_cv.fit(X_train, y_train.values.ravel())
grid_pred = grid_cv.predict(X_test)
grid_pred.to_csv("Grid_sub.csv", mode='w')

Modeling_MA&EPMA&ARIMA

Moving Average

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45

def dataset_month(data):
# raw data 를 month 와 year 기준으로 resampling
dataset =pd.DataFrame([])

for i in tqdm(data.store_id.unique()):
store_zero = data[data.store_id==i]
store_zero['transacted_date'] = pd.to_datetime(store_zero['transacted_date'])
pd.to_datetime(store_zero['transacted_date'])

store_zero['month'] =store_zero['transacted_date'].dt.month
store_zero['year'] =store_zero['transacted_date'].dt.year

store_zero_1 = store_zero.drop(['card_id','card_company','transacted_date','transacted_time', 'installment_term', 'region','type_of_business'],axis=1)
store_zero_group = store_zero_1.groupby(['store_id','year','month'] ).sum().reset_index()
store_zero_group['amount'].sum() #24174471
dataset= pd.concat([dataset, store_zero_group],axis=0)
return dataset

d1 = dataset_month(sales)

def MA_dataset(data):

# 이동평균(3,5) 변수 추가
blank = pd.DataFrame([])

for i in tqdm(data.store_id.unique() ):
store = data[data.store_id == i]
#rolling mean 3,5
windows = [3,5]
for window in windows:
store['rolling_mean_' + str(window)] =store.amount.rolling(window=window).mean()

blank = pd.concat([blank,store], axis=0)
blank = blank.reset_index()
blank = blank.drop( ['index'], axis=1)

for j in blank.store_id.unique():
X_test_blank = pd.DataFrame( [[0 for i in range(6)] for i in range(3)], columns=['store_id', 'year', 'month', 'amount','rolling_mean_3','rolling_mean_5' ] )
X_test_blank.store_id = j
blank = pd.concat( [blank, X_test_blank], axis=0)

return blank

new = MA_dataset(d1)
       <단순이동평균> 

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
def MA_prediction(data):

# 이동평균(3) 을 활용한 예측 모델링, 가중치값 1/3
blank = pd.DataFrame([])
for i in tqdm(data.store_id.unique()):
store_id = i
store = data[data['store_id'] == i]

# MA Caculation
ma1 = store.rolling_mean_3[-4:-3].values
store.amount[-3:-2] = ma1

store.amount[-2:-1] = (ma1 + store.amount[-4:-3].values + store.amount[-5:-4].values )*1/3
store.amount[-1:] = (store.amount[-2:-1].values + ma1 + store.amount[-4:-3].values )*1/3

store_pred = store.amount[-3:].sum()
dictA = { 'store_id' : [store_id], 'amount' : [store_pred] }
df1 = pd.DataFrame( dictA )

blank = pd.concat([blank,df1],axis=0)
return blank


MA_result_2 = MA_prediction(new)
MA_result_2.to_csv("submission_MA3_1015.csv", mode='w')
       <지수이동평균> 

Exponential Moving Average

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def Exponential_MA_prediction(data,n):

# 지수이동평균의 경우 예측모형으로 사용할 때의 가중치(w) 값은 2/(n+1)
blank = pd.DataFrame([])
for i in tqdm(data.store_id.unique()):
store_id = i
store = data[data['store_id'] == i]
store['exponential_MA_5'] = store.amount.ewm(span=n, adjust=False).mean()

exma = store.exponential_MA_5[-4:-3].values
exma_sum = exma*3

dictA = { 'store_id' : [i], 'amount' : exma_sum }
df1 = pd.DataFrame( dictA )
blank = pd.concat([blank,df1],axis=0)
return blank

EX_MA_pred_5 = Exponential_MA_prediction(new,5)
EX_MA_pred_3 = Exponential_MA_prediction(new,3)

EX_MA_pred_5.to_csv("submission_EX_MA5_1015_1.csv", mode='w')
EX_MA_pred_3.to_csv("submission_EX_MA3_1015_1.csv", mode='w')

ARIMA

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
# ARIMA Model 

# 각각의 상점마다 다른 pdq 값을 적용하기 위해서, autoarima model과 유사한 알고리즘을 사용하였다.

import itertools

p = [0,1,2]
d = [0,1,2]
q = [0,1,2]

pdq = list(itertools.product(p, d, q))

from statsmodels.tsa.arima_model import ARIMA

def arima_modeling_5(data):
arima_pred_arr = np.array([])
for i in tqdm(data.store_id.unique()):
data_set = data[data.store_id == i]

best_score = 10000000000
best_param = 0
for param in pdq:
try:
arima_model = ARIMA(data_set.amount.values, order=param)
result = arima_model.fit()
if result.aic < best_score:
best_score = result.aic
best_param = param
except:
continue

arima_model = ARIMA(data_set.amount.values, order=best_param)
arima_result = arima_model.fit()
arima_pred = arima_result.forecast(3)[0]

arima_pred_arr = np.concatenate((arima_pred_arr, np.array([arima_pred.sum()])))
return arima_pred_arr


arima_result_month_all_data = arima_modeling_5(d1)

arima_result_month_all_data = pd.DataFrame(arima_result_month_all_data)
arima_result_month_all_data.to_csv("arima_result_submission_1015.csv", mode='w')

Conclusion

  • 최종결과
    Model MAE(Mean Absolute Error,
    평균절대오차)
    Public Board Rank
    Exponential_Moving Average(5) 765,887 9등(최종)
    Exponential_Moving Average(3) 784,219 18등
    Simple_Moving_Average(3) 810,333 25등
    ARIMA_n_LGBM 1,768,874 93등
    ARIMA 1,062,635 50등
    LGBM 2,481,832 102등
  1. Data size

해당 데이터의 경우 data size 에 관한 issue 가 있었다.
처음 제공된 raw data 을 탐색하였을 때, 그 사이즈가 6,556,613 rows 로 머신러닝에 활용하기에 충분한 볼륨이 있어 보였다.
하지만 데이터는 개별 카드의 결제 건당 매출을 기준으로 작성되었고, 대회의 최종목표는 상점별 매출을 예측하는 것이었으므로
resampling 과정이 불가피 하였다. card_id 는 store_id 기준으로, day 는 month 기준으로 데이터 전처리를 하였을 때,
6,556,613 rows 에서 60,232 rows 로 데이터의 사이즈가 약 1/100 가량 축소되었다.

  1. Data variables

raw data 에서 주어진 column 값들 중, store 를 구분 할 수 있는 categorical variable 은 ‘region’ 과 ‘type_of_business’
두 가지가 유일하였다. 하지만 그마저도 결측치의 비율이 65%에 달하여 전체 데이터셋에 일반화하여 적용하기에 무리가 있었다.
따라서 데이터셋을 결측치 유무에 따른 두 가지의 그룹으로 나누어서 분석을 진행하였다.

  1. Modeling

3.1 LGBM

‘region’ 과 ‘type_of_business’가 존재하는 데이터셋을 group1 로 지정하고 모델링을 진행하였다. group1 의 경우에,
146개의 전체 업종 중에서 매출 top10 업종이 전체 매출에서 차지하는 비중은 50% 였으며, top1 인 한식음식점업의
경우에 전체 매출의 16%를 차지하였다. 따라서 한식음식점 및 상위 업종의 매출 trend 및 seasonality 를 중심으로
모델링 하기 위하여, Feature Engineering 과정에서 이동평균과 lag 을 적극적으로 활용하여 변수에 추가하였다.
결과적으로 LGBM 모델링의 MAE 값은 2,481,832 로 대회 Public Board 기준으로는 102등(최하위권)을 기록하였다.
tree 모델을 사용하기에는 다소 부족한 데이터의 수와, categorical variable 의 부재로 인한 결과로 추측된다.

3.2 Simple Moving Average, Exponential Moving Average and ARIMA

머신러닝을 활용한 모델링이 힘들기 때문에, 전통적인 방식의 시계열 분석 방법들을 시도해 보았다.
결론적으로 Exponential Moving Average(5) 에 의한 결과값이 가장 좋았다. MAE 값은 765,887 로 대회 Public Board 기준으로 9등(최상위권)
을 달성하였고, Simple Moving Average(3)가 25등, ARIMA가 50등의 성적을 보여주었다.

현재 데이터의 특성을 전적으로 보여주는 결과라고 생각을 한다. 각각의 모델은 아래와 같은 특징을 지닌다.
Simple Moving Average (단순이동평균) 의 경우에, 각각의 시계열에 동일한 가중치를 두고 종속변수의 평균값 및 예측값을 계산한다.
따라서 계절성이 적고 추세성이 강한 데이터를 잘 예측할 확률이 높다.

Exponential MA (지수이동평균) 의 경우에는 예측시점에 가까운 데이터일수록 가중치를 강하게 가져가고, 예측시점에서 먼 데이터일수록
가중치를 낮게 준다. 따라서 단순이동평균에 비해서 최근의 데이터 변화에 민감한 예측을 하게 된다.

ARIMA 모델의 경우에는 예측값들이 가지는 관계성에 비중을 두는 모델이다. 따라서 패턴성이 강하고 시계열이 길게 주어질 수록
예측력이 높아지는 경향이 있다.

앞서 group1 에 관한 분석과 같이, 한식음식점업이 업종이 드러나지 않은 데이터 전체에서 차지하는 비중이 매우 크다고 가정할 때
한식음식점업이 가지는 데이터의 패턴은 데이터 전체에 영향을 끼칠 확률이 높다. 따라서 계절성과 추세성이 약한 요식업의 특성상,
Exponential MA (지수이동평균) 모델이 현 데이터를 가장 잘 예측한 것이 아닐까 추론해 보았다.

Cos Pro Python 1급 1차 10번 문제풀이

Contents of table:


Topic: 주식으로 최대 수익을 내세요

Review:
Solution 함수를 역으로 훑어오면은, price=3 일때, mn 값이 1여야 하므로, 해당 아이디어를 가지고 수식을 훑으면
수익률을 return 하는 부분인, mn - price가 아닌, price-mn 이 되어야 함을 알 수 있다.


정답코드


1
2
3
4
5
6
7
8
9
10
11

def solution(prices):
inf = 1000000001;
mn = inf
ans = -inf
for price in prices: #price 1 , price2, mn = 1 , price3, mn = 1
if mn != inf: #mn =1001 , inf = 1001 // mn =1, inf = 1001 // mn=1, inf =1001
ans = max(ans, price - mn) # 문제 코드에서는 mn - price 로 되어있다. ans = max( -1001, 2-1)= 1 // ans = max(1, 3 - 1) = 2
mn = min(mn, price) #price 1, mn = 1
return ans

문제내용

문제10

지난 연속된 n일 동안의 주식 가격이 순서대로 들어있는 리스트가 있습니다. 이때, 다음 규칙에 따라 주식을 사고 팔았을 때의 최대 수익을 구하려 합니다.

  • n일 동안 주식을 단 한 번 살 수 있습니다.
  • n일 동안 주식을 단 한 번 팔 수 있습니다.
  • 주식을 산 날에 바로 팔 수는 없으며, 최소 하루가 지나야 팔 수 있습니다.
  • 적어도 한 번은 주식을 사야하며, 한 번은 팔아야 합니다.

주식을 팔 때는 반드시 이전에 주식을 샀어야 하며, 최대 수익은 양수가 아닐 수도 있습니다.

연속된 n 일 동안의 주식 가격이 순서대로 들어있는 리스트 prices가 매개변수로 주어질 때, 주식을 규칙에 맞게 한 번만 사고팔았을 때 얻을 수 있는 최대 수익을 return 하도록 solution 함수를 작성했습니다. 그러나, 코드 일부분이 잘못되어있기 때문에, 코드가 올바르게 동작하지 않습니다. 주어진 코드에서 _한 줄_만 변경해서 모든 입력에 대해 올바르게 동작하도록 수정해주세요.


매개변수 설명

연속된 n 일 동안의 주식 가격이 순서대로 들어있는 리스트 prices가 solution 함수의 매개변수로 주어집니다.

  • prices의 길이는 2 이상 1,000,000 이하입니다.
  • prices의 각 원소는 1 이상 1,000 이하의 자연수입니다.

#####return 값 설명
주식을 규칙에 맞게 한 번만 사고팔았을 때 얻을 수 있는 최대 수익을 return 해주세요.


예시

prices return
[1,2,3] 2
[3,1] -2

예시 설명

예시 #1
연속된 3일의 주가가 차례로 [1, 2, 3] 이며, 첫째 날에 주식을 사서 셋째 날에 팔면 수익은 2이고, 이때가 최대입니다.

예시 #2
문제에서 설명한 것처럼 무조건 한 번은 매수하고, 한 번은 매도해야 합니다. 첫째 날에 매수하여 둘째 날에 매도하는 방법밖에 없기 때문에 수익은 -2, 즉 2만큼 손실을 보게 됩니다.