1. Stock Prediction Part.1 - baseline model

1-1. 머신러닝을 위한 주가 데이터셋 생성

주가 데이터셋 생성 절차는 df2list - dictionary - MultiProcessing - MySQL 순서로 진행한다. 4가지 절차를 통해 데이터셋이 달라지는 것은 아니고, 속도를 개선시키는 방향으로의 효율적인 코드를 짜기 위한 훈련 과정이다. 대규모 데이터셋을 다루기 위해서는 효율적인 코딩을 통해 속도를 개선하는 것도 중요한 일이다. 특히 이번에 사용하는 주가 데이터셋은 양이 많을 뿐만 아니라, fdr 라이브러리를 사용하여 외부에서 불러와야하기 때문에 데이터셋을 구성하는 것만 해도 속도가 상당히 느리다. 이러한 문제점을 해결하고자 본 포스팅에서는 4단계에 걸친 데이터셋 생성 과정을 보여준다.


데이터셋 정의


데이터셋 생성 과제

데이터셋 생성을 총 3가지 과제로 나누어 진행한다.


목차

필요 라이브러리 import

# finance datareader 설치 
# ! pip install -U finance-datareader
import pandas as pd
import os
from tqdm import tqdm
import FinanceDataReader as fdr
import time

# 경고 메시지 무시 
import warnings
warnings.filterwarnings(action='ignore')

✔️ (0) Finance Data Reader를 이용한 주가 데이터셋 - DataFrame

속도 비교를 위해 기존 데이터의 타입인 DataFrame을 사용하여 데이터셋을 생성한다. 연구 인턴 초기 첫번째로 작성했던 코드들인데, 개선해야 할 부분들이 많다.

과제 I

df = pd.read_html('http://kind.krx.co.kr/corpgeneral/corpList.do?method=download', header=0)[0]

# 회사명, 종목코드, 상장일 컬럼만 사용 
df_code = df[['회사명', '종목코드', '상장일']]

# 종목코드를 6자리로 맞춰 준다. 
df_code['종목코드'] = df_code['종목코드'].apply(lambda x : str(x).zfill(6))
display(df_code.head(3))
print()

# 상장일이 2018년 1월 1일 이전인 종목코드 선별 
start_time = time.time()
lst_code = df_code.loc[df_code['상장일'] < '2018-01-01', '종목코드'].to_list()
print("걸린 시간: ", time.time() - start_time)
print()
print('상장일 2018-01-01 이전 종목코드: ', lst_code[:5])
print()
print(f'총 {len(df_code)} 개의 종목 중 {len(lst_code)} 개의 종목 선별')
회사명 종목코드 상장일
0 DL 000210 1976-02-02
1 DRB동일 004840 1976-05-21
2 DSR 155660 2013-05-15
걸린 시간:  0.0006933212280273438

상장일 2018-01-01 이전 종목코드:  ['000210', '004840', '155660', '078930', '001390']

총 2507 개의 종목 중 1977 개의 종목 선별

과제 II

stock_dict = {}
for code in tqdm(lst_code): 
    stock = fdr.DataReader(code, start='20180101', end='20201231')
    stock['trading'] = stock['Volume'] * stock['Close'] # 거래대금 컬럼 추가
    
    if sum(stock['trading'] >= 100000000000) >= 1: # 거래대금이 1000억 이상인 데이터가 하나 이상 존재하면
        stock_dict[code] = stock[stock['trading'] >= 100000000000].index 

print(f'총 {len(lst_code)} 개의 종목 중 {len(stock_dict)} 개의 종목 사용')
100%|██████████████████████████████████████████████| 1977/1977 [02:58<00:00, 11.08it/s]

총 1977 개의 종목 중 799 개의 종목 사용
# 선별된 종목과 날짜를 lst_code_date에 넣어준다. 
lst_code_date = []
for code in tqdm(stock_dict): 
    for date in (stock_dict[code]):
        lst_code_date.append([code, date])
        
print(f'선별된 날짜는 총 {len(lst_code_date)}개')
100%|█████████████████████████████████████████████| 799/799 [00:00<00:00, 13125.89it/s]

선별된 날짜는 총 14182개

과제 III

data_dict = {'code': [], 'd0': [], 'info': [], 'up': []}
for code, date in tqdm(lst_code_date):
    start_date = '20171201' # 2018년 초반 날짜가 D0라면 2017년 데이터 필요 (D-9~D-1)
    end_date = '20210130' # 2020년 후반 날짜가 D0라면 2021년 데이터 필요 (D+1) 
    stock = fdr.DataReader(code, start = start_date, end = end_date)
    stock.reset_index(inplace=True) # 'Date' index -> column 
    
    D9_index = stock[stock['Date'] == str(date)].index[0] - 9 # D-9 날짜의 인덱스  
    next_index = stock[stock['Date'] == str(date)].index[0] + 1 # D+1 날짜의 인덱스  
        
    # 종목코드 (code)
    data_dict['code'].append(code) 
    
    # 기준일 (d0)
    data_dict['d0'].append(date)
    
    # D-9 ~ D+1, 총 11일치의 sub stock DataFrame 생성 
    sub_stock = stock.iloc[D9_index:next_index+1]
    sub_stock['trading'] = sub_stock['Close'] * sub_stock['Volume'] # 거래대금 컬럼 추가 
   
    
    # 10일 간의 데이터 (info)
    info_list = []
    for i in range(10):        
        info_list.append(sub_stock.iloc[i, [1, 2, 3, 4, -1]].to_list())
    remove_list=['[', ']']
    for i in range(2): 
        info_list = f'{info_list}'.replace(remove_list[i], '')
    data_dict['info'].append(info_list)    
        
        
    # D+1 종가 2% 상승 여부 (up)
    up = sub_stock.iloc[-2]['Close'] + 0.02 * sub_stock.iloc[-2]['Close']
    
    if sub_stock.iloc[-1]['Close'] >= up: 
        data_dict['up'].append(1)
    else: 
        data_dict['up'].append(0)
100%|████████████████████████████████████████████| 14182/14182 [17:08<00:00, 13.78it/s]
df_result = pd.DataFrame(data_dict)
display(df_result.head()) 

# 최종 결과 데이터셋 txt 파일 저장 
df_result.to_csv("assignment3.txt")
print(f'생성된 데이터의 개수는 {len(pd.read_csv("assignment3.txt"))} 개')
code d0 info up
0 000210 2018-01-26 78343, 78614, 76987, 77892, 9590608284, 77801,... 0
1 000210 2018-08-08 68855, 69397, 67590, 69036, 6067435968, 69487,... 0
2 000210 2020-04-02 44819, 52951, 44322, 51145, 15615437965, 47439... 0
3 000210 2020-09-11 80783, 84487, 78524, 78524, 61554885076, 79608... 0
4 000210 2020-12-11 76174, 76174, 72289, 72289, 76043112348, 72831... 0
생성된 데이터의 개수는 14182 개

개선해야할 사항


✔️ (1) Finance Data Reader를 이용한 주가 데이터셋 - df2list

첫번째 방법은 DataFrame 사용을 지양하고, python의 기본 데이터 타입인 list로 바꾸어 사용하는 방법이다. 이로써 column 중심의 연산을 row 중심의 연산으로 바꾸어준다. 이 방법에서는 속도 개선 보다는 (0)번 방법의 코드에서 효율적이지 못했던 부분을 고치고 깔끔한 코드로 보완한다.

과제 I

# (0) 방법에서 사용했던 df_code 데이터 프레임 사용 
display(df_code.head(2))

# 🌟 dataframe -> list 
lst_stock = df_code.values.tolist()
print(lst_stock[:2])
print()


lst_code = [] # 선별 된 코드를 담을 리스트 
start_time = time.time()
for row in lst_stock:
    code, date = row[1], row[2]
    if date <= '2018-01-01':
        lst_code.append(code)
print("걸린 시간: ", time.time() - start_time)
print()

        
print('상장일 2018-01-01 이전 종목코드: ', lst_code[:4])
print()
print(f'총 {len(df_code)} 개의 종목 중 {len(lst_code)} 개의 종목 선별')
회사명 종목코드 상장일
0 DL 000210 1976-02-02
1 DRB동일 004840 1976-05-21
[['DL', '000210', '1976-02-02'], ['DRB동일', '004840', '1976-05-21']]

걸린 시간:  0.0004284381866455078

상장일 2018-01-01 이전 종목코드:  ['000210', '004840', '155660', '078930']

총 2507 개의 종목 중 1977 개의 종목 선별

과제 II

lst_code_date = []
for code in tqdm(lst_code):
    stock = fdr.DataReader(code, start='20180102', end='20201231')
    stock.reset_index(inplace=True)
    
    # 🌟 dataframe -> list 
    lst_stock = stock.values.tolist()
    
    for row in lst_stock: 
        date, trading_value = row[0], row[4]*row[5]
        if trading_value >= 100000000000:  # 거래대금 1000억 이상
            lst_code_date.append([code, date.date().strftime("%Y%m%d")])
            
print(f'선별된 날짜는 총 {len(lst_code_date)}개')
100%|██████████████████████████████████████████████| 1977/1977 [02:55<00:00, 11.26it/s]

선별된 날짜는 총 14182개

과제 III

OF = open('assignment3.txt','w')

for code, date in tqdm(lst_code_date):
    start_date = '20180101' 
    end_date = '20201231' 
    stock = fdr.DataReader(code, start = start_date, end = end_date)
    stock.reset_index(inplace=True) # 'Date' index -> column 
    
    
    # 🌟 dataframe -> list 
    lst_stock = stock.values.tolist()
    
    
    for idx, row in enumerate(lst_stock): 
        if (idx < 9) or (idx >= len(lst_stock)-1): # 예외 처리 
            continue 
        
        if row[0].date().strftime("%Y%m%d") == date: 
            
            # D-9 ~ D0 데이터만 담기
            sub_stock = lst_stock[idx-9:idx+1]
            
            # 10일 간의 데이터 
            lst_info = []
            for row2 in sub_stock:
                lst_prices, trading_value = row2[1:5], row[4]*row[5]
                lst_info += lst_prices + [trading_value]
                
            info = ','.join(map(str, lst_info))
            
            # D+1 종가 2% 상승 여부 (up)
            change = lst_stock[idx+1][6]
            label = int(change >= 0.02)
            
            # 저장 
            OF.write(f'{code}\t{date}\t{lst_info}\t{label}\n')
            
OF.close()

print(f'생성된 데이터의 개수는 {len(pd.read_csv("assignment3.txt"))} 개')
100%|████████████████████████████████████████████| 14182/14182 [17:43<00:00, 13.33it/s]

생성된 데이터의 개수는 13939 개

✔️ (2) Finance Data Reader를 이용한 주가 데이터셋 - dictionary

dictionary를 사용하는 방법은 과제III 의 속도를 개선한다. fdr 라이브러리를 최소한으로 사용하고, for문을 최대한 줄인다. 현재 문제점은 날짜를 기준으로 for문이 돌아가기 때문에 같은 데이터(같은 종목)가 여러번 불러와지는 경우가 다수 존재한다는 것이다. 따라서 과제II 에서 codeD0 날짜 리스트를 dictionary 타입으로 생성하고, 과제III 에서 code 당 한번만 fdr 라이브러리를 사용하도록 바꾸어 준다.

과제 I

과제I 은 앞의 방법과 같으므로 생략한다.

print('상장일 2018-01-01 이전 종목코드: ', lst_code[:4])
print()
print(f'총 {len(df_code)} 개의 종목 중 {len(lst_code)} 개의 종목 선별')
상장일 2018-01-01 이전 종목코드:  ['000210', '004840', '155660', '078930']

총 2507 개의 종목 중 1977 개의 종목 선별

과제 II

dict_code2date = {}
for code in tqdm(lst_code): 
    start_date = '20180102'
    end_date = '20201231'
    stock = fdr.DataReader(code, start = start_date, end = end_date)
    stock.reset_index(inplace=True)    
    
    # 🌟 dataframe -> list     
    lst_stock = stock.values.tolist()
    
    for row in lst_stock: 
        date, trading_value = row[0], row[4]*row[5]
        if trading_value >= 100000000000:
            if code not in dict_code2date.keys():
                dict_code2date[code] = [date.date().strftime("%Y%m%d")]
            else:
                dict_code2date[code].append(date.date().strftime("%Y%m%d"))

print(f'총 {len(lst_code)} 개의 종목 중 {len(dict_code2date)} 개의 종목 사용')
100%|██████████████████████████████████████████████| 1977/1977 [02:42<00:00, 12.14it/s]

총 1977 개의 종목 중 799 개의 종목 사용

과제 III

OF = open('assignment3.txt', 'w')
for code in tqdm(dict_code2date): 
    # code의 stock 
    start_date = '20180101' 
    end_date = '20201231' 
    stock = fdr.DataReader(code, start = start_date, end = end_date)
    stock.reset_index(inplace=True)
    
    # 🌟 dataframe -> list     
    lst_stock = stock.values.tolist()  
       
    for idx, row in enumerate(lst_stock):   
        if (idx < 9) or (idx >= len(lst_stock)-1): # 예외 처리 
            continue 
        
        date = row[0].date().strftime("%Y%m%d") 
        if date not in dict_code2date[code]: # 조건에 부합하는 날짜 (D0 날짜)를 발견할 때까지 continue
            continue 

        # D-9 ~ D0 데이터만 담기
        sub_stock = lst_stock[idx-9:idx+1] 
        
        # 10일간의 데이터 
        lst_info = []
        for row2 in sub_stock:
            lst_prices, trading_value = row2[1:5], row2[4]*row2[5]
            lst_info += lst_prices + [trading_value]
        info = ','.join(map(str, lst_info))

        # D+1 종가 2% 상승 여부 
        label = int(lst_stock[idx+1][6] >= 0.02)

        # 저장 
        OF.write(f'{code}\t{date}\t{info}\t{label}\n')
                         
OF.close()   

print(f'생성된 데이터의 개수는 {len(pd.read_csv("assignment3.txt"))} 개')
100%|████████████████████████████████████████████████| 799/799 [01:07<00:00, 11.84it/s]

생성된 데이터의 개수는 13939 개

약 17분이 걸리던 시간이 1분으로 줄어들었다.


✔️ (3) Finance Data Reader를 이용한 주가 데이터셋 - MultiProcessing

pythonmultiprocessing 라이브러리를 통해 다중 처리를 지원한다. 여러 개의 코어를 연산에 사용함으로써 많은 작업을 빠른 시간에 처리해줄 수 있다는 장점이 있다.

image.png [ core=10으로 설정하여 multi processing을 수행할 때 코어 사용 ]

# MultiProcessing을 위한 library import 
import time, os
from multiprocessing import Pool

과제 I

과제I 은 앞의 방법과 같으므로 생략한다.

print('상장일 2018-01-01 이전 종목코드: ', lst_code[:4])
print()
print(f'총 {len(df_code)} 개의 종목 중 {len(lst_code)} 개의 종목 선별')
상장일 2018-01-01 이전 종목코드:  ['000210', '004840', '155660', '078930']

총 2507 개의 종목 중 1977 개의 종목 선별

과제 II

def make_lst_result(code): 
    start_date = '20180101'
    end_date = '20201231'
    
    lst_date = []
    
    stock = fdr.DataReader(code, start = start_date, end = end_date)
    stock.reset_index(inplace=True)
  
    # 🌟 dataframe -> list
    lst_stock = stock.values.tolist()
    
    for row in lst_stock: 
        if row[4] * row[5] >= 100000000000: 
            lst_date.append(row[0].date().strftime("%Y%m%d"))
        
    return [code, lst_date]
start_time = time.time()
num_cores = 10
pool = Pool(num_cores)
lst_code_date = pool.map(make_lst_result, lst_code)
pool.close()
pool.join()
print(time.time() - start_time) 
17.97966194152832

앞서 약 2분 40초 가 걸렸던 과제IImulti processing을 사용하여 17초대로 단축시켰다.

dict_code2date = {}

for code, lst_date  in tqdm(lst_code_date):
    if lst_date == []:
        continue
    dict_code2date[code] = lst_date
            
print(f'총 {len(lst_code)} 개의 종목 중 {len(dict_code2date)} 개의 종목 사용')
100%|█████████████████████████████████████████| 1977/1977 [00:00<00:00, 1610124.08it/s]

총 1977 개의 종목 중 799 개의 종목 사용

과제 III

def make_lst_result2(code): 
    # code의 stock 
    start_date = '20180101' 
    end_date = '20201231' 
    stock = fdr.DataReader(code, start = start_date, end = end_date)
    stock.reset_index(inplace=True)
    
    # 🌟 dataframe -> list     
    lst_stock = stock.values.tolist()  
       
    lst_result = []
        
    for idx, row in enumerate(lst_stock): 
        
        if (idx < 9) or (idx >= len(lst_stock)-1): # 예외 처리 
            continue 
            
        date = row[0].date().strftime("%Y%m%d") 
        if date not in dict_code2date[code]: # 조건에 부합하는 날짜 (D0 날짜)를 발견할 때까지 continue
            continue 

        # D-9 ~ D0 데이터만 담기
        sub_stock = lst_stock[idx-9:idx+1] 
        
        # 10일간의 데이터 
        lst_info = []
        for row2 in sub_stock:
            lst_prices, trading_value = row2[1:5], row2[4]*row2[5]
            lst_info += lst_prices + [trading_value]
        info = ','.join(map(str, lst_info))

        # D+1 종가 2% 상승 여부 
        label = int(lst_stock[idx+1][6] >= 0.02)
        
        lst_result.append([code, date, info, label])
        
    return lst_result
start_time = time.time()
num_cores = 10
pool = Pool(num_cores)
lst_data = pool.map(make_lst_result2, dict_code2date.keys())
pool.close()
pool.join()
print(time.time() - start_time) 
8.186521768569946

앞서 진행했던 방법에서 1분이 걸렸던 작업이 multi processing을 통하여 8초로 줄어들었다.

OF = open("assignment3_multi_processing.txt", 'w')

for row in lst_data: 
    for num in range(len(row)): 
        OF.write('\t'.join(map(str, row[num])) + '\n')
        
OF.close()

print(f'생성된 데이터의 개수는 {len(pd.read_csv("assignment3_multi_processing.txt"))} 개')
생성된 데이터의 개수는 13939 개

✔️ (4) Finance Data Reader를 이용한 주가 데이터셋 - MySQL

4번째 방법은 MySQL을 사용한다. 외부 데이터를 불러오지 않아도 되고, 서버 DB에 저장된 데이터를 불러오는 것이므로 multi processing을 사용하지 않고도 빠른 속도로 데이터셋을 생성할 수 있다.

과제 I

최종적으로 머신러닝 분석에 사용할 데이터셋은 코스피, 코스닥 시장에 해당하는 종목들만을 사용한다. 현재 db에 저장되어 있는 데이터도 코스피, 코스닥 시장에 해당하는 종목들이 입력되어 있으며, 해당 종목들을 추린 code_list.txt에서 종목들을 불러와 lst_code를 사용한다.

IF = open('../data/code_list.txt')
lst_code = IF.readlines()

print(f'총 {len(df_code)} 개의 종목 중 {len(lst_code)} 개의 종목 선별')
총 2507 개의 종목 중 1561 개의 종목 선별
# pymysql 설치
# ! pip install pymysql

import pymysql 
from sqlalchemy import create_engine

code 별로 다른 테이블에 저장한다.

db_connection_str = 'mysql+pymysql://[db username]:[db password]@[host address]/[db name]' 
db_connection = create_engine(db_connection_str)
conn = db_connection.connect()

for code in tqdm(lst_code): 
    start_date = '20170101'
    end_date = '20211231'
    stock = fdr.DataReader(code, start = start_date, end = end_date)
    stock = stock.reset_index()
    stock = stock[['Date', 'Open', 'High', 'Low', 'Close', 'Volume', 'Change']]
    stock.to_sql(name=f'stock_{code}', con=db_connection, if_exists='fail', index=False)
db_dsml = pymysql.connect(
    host = 'localhost', 
    port = 3306, 
    user = '[db username]', 
    passwd = '[db password]', 
    db = '[db name]', 
    charset = 'utf8'
)
cursor = db_dsml.cursor()

과제 II

dict_code2date = {}
for code in tqdm(lst_code): 
    code = code.strip()
    sql_query = '''
                SELECT *
                FROM stock_{}
                WHERE Date BETWEEN '2018-01-01' AND '2020-12-31'
                '''.format(code)
    stock = pd.read_sql(sql = sql_query, con = db_dsml)   
    
    # 🌟 dataframe -> list     
    lst_stock = stock.values.tolist()
    
    for row in lst_stock: 
        date, trading_value = row[0], row[4]*row[5]
        if trading_value >= 100000000000:
            if code not in dict_code2date.keys():
                dict_code2date[code] = [date.date().strftime("%Y%m%d")]
            else:
                dict_code2date[code].append(date.date().strftime("%Y%m%d"))

print(f'총 {len(lst_code)} 개의 종목 중 {len(dict_code2date)} 개의 종목 사용')
100%|██████████████████████████████████████████████| 1561/1561 [00:22<00:00, 69.07it/s]

총 1561 개의 종목 중 679 개의 종목 사용

과제 III

OF = open('assignment3_sql.txt', 'w')
for code in tqdm(dict_code2date): 
    code = code.strip()
    sql_query = '''
                SELECT *
                FROM stock_{}
                WHERE Date BETWEEN '2018-01-01' AND '2020-12-31'
                '''.format(code)
    stock = pd.read_sql(sql = sql_query, con = db_dsml)  
    
    # 🌟 dataframe -> list     
    lst_stock = stock.values.tolist()  
       
    for idx, row in enumerate(lst_stock):   
        if (idx < 9) or (idx >= len(lst_stock)-1): # 예외 처리 
            continue 
        
        date = row[0].date().strftime("%Y%m%d") 
        if date not in dict_code2date[code]: # 조건에 부합하는 날짜 (D0 날짜)를 발견할 때까지 continue
            continue 

        # D-9 ~ D0 데이터만 담기
        sub_stock = lst_stock[idx-9:idx+1] 
        
        # 10일간의 데이터 
        lst_info = []
        for row2 in sub_stock:
            lst_prices, trading_value = row2[1:5], row2[4]*row2[5]
            lst_info += lst_prices + [trading_value]
        info = ','.join(map(str, lst_info))

        # D+1 종가 2% 상승 여부 
        label = int(lst_stock[idx+1][6] >= 0.02)

        # 저장 
        OF.write(f'{code}\t{date}\t{info}\t{label}\n')
                         
OF.close()   

print(f'생성된 데이터의 개수는 {len(pd.read_csv("assignment3_sql.txt"))} 개')
100%|████████████████████████████████████████████████| 679/679 [00:11<00:00, 61.46it/s]

생성된 데이터의 개수는 11934 개

✔️ (5) 최종 머신러닝 데이터셋

최종적으로 생성된 머신러닝 데이터셋의 형태를 확인한다.

IF=open("assignment3_sql.txt",'r')
lst_code_date=[]
trainX=[]
trainY=[]
for line in IF:
    code, date, x, y = line.strip().split("\t")
    lst_code_date.append([code, date])
    trainX.append(list(map(int, x.split(","))))
    trainY.append(int(y))
trainX=pd.DataFrame(trainX)
trainY=pd.DataFrame(trainY)
print("===== trainX =====")
print("trainX shape:", trainX.shape)
display(trainX.head())
print()
print("===== trainY =====")
print("trainY shape:", trainY.shape)
display(trainY.head())
===== trainX =====
trainX shape: (11935, 50)
0 1 2 3 4 5 6 7 8 9 ... 40 41 42 43 44 45 46 47 48 49
0 10250 12050 10150 11800 307823874200 11950 12450 10900 11750 240410569500 ... 15300 15400 12650 13700 789063638200 13700 16100 13400 15400 897154258000
1 11950 12450 10900 11750 240410569500 11850 14150 11600 12600 764364560400 ... 13700 16100 13400 15400 897154258000 14700 15500 14000 14350 277027065700
2 11850 14150 11600 12600 764364560400 12800 13200 12000 12200 170010147600 ... 14700 15500 14000 14350 277027065700 13050 13300 11650 11650 231873876050
3 12800 13200 12000 12200 170010147600 12450 13400 12350 12850 211661434950 ... 13050 13300 11650 11650 231873876050 12200 13150 11600 12200 222393934200
4 12450 13400 12350 12850 211661434950 12800 12950 11300 11700 91801277100 ... 12200 13150 11600 12200 222393934200 12200 13750 12100 12350 256196958550

5 rows × 50 columns

===== trainY =====
trainY shape: (11935, 1)
0
0 0
1 0
2 1
3 0
4 1

4단계의 과정을 거쳐 머신러닝 데이터셋 생성을 마쳤다. 다음 글에서는 생성된 머신러닝 데이터셋을 사용하여 여러 머신러닝 모델을 학습 및 평가하여 성능이 가장 좋은 모델을 선정하는 baseline model selection을 진행한다.