ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • PER PBR 활용06 : 기업별 산업코드 PER PBR 데이터 생성 프로그램 CLASS 구성
    금융퀀트/(퀀트)PERPBR활용 2023. 6. 10. 18:18
    반응형

    python CLASS의 사용

    python CLASS 사용의 필요성

    기업별, 산업별 PER, PBR 분석을 위한 기초 과정을 정리하면 아래와 같다.
    STEP1. PER, PBR 산출 대상이 되는 주식목록을 입수(PER PBR 활용02 : 주식목록 입수(공공데이터API))
    STEP2. 입수된 주식 목록에 대한 PER, PBR 데이터를 입수(PER PBR 활용01 : PER PBR 데이터 입수(파이썬))
    STEP3. DART기업고유번호-주식종목코드, 산업코드-산업코드명 매핑 테이블 생성(PER PBR 활용05 : DART기업고유번호매핑 및 산업코드 매핑)
    STEP4. DART API와 매핑 테이블을 이용해서 주식 목록에 대한 산업정보 입수(PER PBR 활용04 : 주식 종목 산업분류 데이터 받아오기(DART API))
    STEP1 ~ STEP4까지의 과정을 종합하면 각 기업의 산업정보, PER, PBR을 모두 볼 수 있는 데이터를 만들어낼 수 있다. STEP1 ~ STEP4 까지의 과정을 합치기 위해서 지금까지 만든 함수들을 기능별로 정리할 필요가 있는데,  클래스를 사용해서 1.데이터 목록 CLASS(GetDf) 2.데이터 MAP CLASS(GetMap) 3.데이터 추가 CLASS(GetAddData) 4.SQL DB 생성 CLASS(PostDf)로 정리할 수 있다.

    CLASS의 세 가지 특성

    클래스는 단순히 함수들을 묶어주는 함수라고 생각하면 된다. 클래스는 일반 함수와는 다르게 아래와 같은 특성을 가진다.
    1. 클래스를 시작할 때(def __init__) 클래스에 속한 함수들이 공유하는 변수들을 설정할 수 있다
    2. 클래스에 속한 함수들은 해당 클래스의 메서드가 되고 "self"를 입력변수로 넣어주어야 한다
    3. 클래스는 그 자체로는 설계도일 뿐이고 인스턴스를 찍어내서 사용할 수 있다. 
    세 가지 특성만 잘 기억한다면 클래스를 통해서 함수를 기능별로 묶어줄 수 있다.

    프로그램 구조도

    함수들을 기능별로 묶는 기준은 개인의 판단이기 때문에 정답은 없다. 아래는 이번 프로젝트의 클래스 및 함수의 구조도인데 이 또한 하나의 예시로 생각하면 될 것 같다.

    그림1: 산업별 PER PBR 구하기 프로그램 구조도

    데이터 목록 CLASS: GetDf

    그림 1에서 처럼 최초데이터 목록을 구성하는 GetDf 클래스는 PER PBR 활용02 : 주식목록 입수(공공데이터API)에서 만들어 본 getStockList 함수를 갖고 있다. getStockList 함수는 공공데이터 포털(https://www.data.go.kr) API를 사용하므로 아래와 같이 해당 api key를 변수로 받는 클래스를 만들면 된다.
     

    import pandas as pd
    from bs4 import BeautifulSoup
    import requests
    
    # Class 선언
    class GetDf:
    	# datagokey 를 변수로 받고 클래스 내에서 self.apikey 라는 이름의 변수로 사용
        def __init__(self, datagokey):
            self.apikey = datagokey
        # getStockList 함수 시작, 페이지수, 출력 줄 수, 기준일자를 변수로 받음
        def getStockList(self, pageno:int, noofrows:str, basedt:str):
        	# 빈 데이터 프레임 설정
            df = []
            # 함수에서 변수로 받은 pageno를 page 에 입력(range(1,3) 이면 작업이 1~2 까지 실행 됨 주의)
            for page in range(1,pageno+1):
            	# API 요청 기본 URL
                url = "https://apis.data.go.kr/1160100/service/GetStockSecuritiesInfoService/getStockPriceInfo"
                # API 요청 Parameter 활용신청 상세기능정보의 요청변수들(함수에서 변수로 받은 값)
                params = {
                    "serviceKey":self.apikey,
                    "pageNo": page,
                    "basDt": basedt,
                    "numOfRows" : noofrows
                }
                # 해당 데이터는 xml 형식으로 제공되어 xml 형식으로 요청함을 header 에 붙임
                headers = {
                    "accept": "application/xml"
                }
                # request 라이브러리로 get 요청을 api 에 보냄
                res = requests.get(url, params=params, headers=headers)
                # response 받은 결과를 soup 에 lxml 형식으로 저장
                soup = BeautifulSoup(res.text, "lxml")
                # soup 객체에 item 별로 내용(text)를 변수에 저장하고 df_if list 에 저장
                for stock in soup.find_all("item"):
                    stockcode = stock.find("srtncd").text #종목코드
                    stockname = stock.find("itmsnm").text #종목명
                    mrkctg =  stock.find("mrktctg").text #시장구분
                    lstgstcnt = stock.find("lstgstcnt").text #상장주식수
                    mrkttotamt = float(stock.find("mrkttotamt").text.replace(',','')) #시가총액
                    df_if = [stockcode, stockname, mrkctg, lstgstcnt, mrkttotamt]
                    # 최초에 만든 df_list 라는 list 에 df_if list를 추가
                    df.append(df_if)
            # 입수된 데이터를 종합하여 DataFrame 형태로 변환 및 컬럼 값 부여
            df = pd.DataFrame(df, columns = ["주식코드", "주식명", "시장구분", "상장주식수", "시가총액"])
            return df

     

    데이터 MAP CLASS: GetMap

    그림 1의 왼쪽에서 두 번째 클래스인 GetMap 클래스는  PER PBR 활용05 : DART기업고유번호매핑 및 산업코드 매핑에서 만들어 본 함수들을 갖고 있다. DART기업고유번호와 주식종목코드의 매핑테이블을 생성하는 함수(configCorpCodeMap)와 매핑테이블을 바탕으로 주식종목코드를 뽑아내는 함수(getCorpCode), 산업코드와 산업코드명의 매핑테이블 생성하는 함수(configIndustMap)와 매핑테이블을 바탕으로 산업코드명을 뽑아내는 함수(getIndustName) 이렇게 4 가지로 구성되어 있는데 매핑테이블을 만들 때 기초파일 경로가 각각 필요하므로 아래와 같이 파일 경로를 변수로 받는 클래스를 만들면 된다.
     

    class GetMap:
        def __init__(self, codepath, indusnamepath):
        	# DART기업코드와 산업코드 매핑한 파일 경로
            self.code_path = codepath
            # 산업코드와 산업코드명을 매핑한 파일 경로
            self.indust_name_path = indusnamepath
        # DART기업코드와 산업코드 매핑한 파일로 DataFrame 생성
        def configCorpCodeMap(self):
        	# 빈 list 만들기
            df = []
            # read 로 파일 열고 xml_data 라는 변수에 저장
            with open(self.code_path, "r", encoding="utf-8") as file:
                xml_data = file.read()
            # 파싱을 위해서 BeautifulSoup 객체로 저장
            soup = BeautifulSoup(xml_data, "lxml")
    		# 각 list tag 를 list 변수에 저장
            for list in soup.find_all("list"):
            	# 각 list tag 안에서 corp_code 를 corp_code 변수에 저장
                corp_code = list.find("corp_code").text
                # 각 list tag 안에서 stock_code 를 stock_code 변수에 저장
                stock_code = list.find("stock_code").text
                # 주식코드가 6 자리로 유효하면 mapping list 에 추가
                if len(stock_code) == 6:
                    df_if = [corp_code, stock_code]
                    df.append(df_if)
                else:
                    pass
            # 완성된 데이터 list 로 DataFrame 구축
            df = pd.DataFrame(df, columns=["DART코드", "주식코드"])
            return df
        # 산업코드와 산업코드명 매핑한 파일로 DataFrame 생성
        def configIndustMap(self):
        	# 엑셀 파일 형식 read 해서 data 변수에 저장
            data = pd.read_excel(self.indust_name_path)
            # 데이터에서 필요한 부분만 뽑아내고, n/a 인 데이터 row 삭제
            data = data.iloc[2:,4:6].dropna()
            # 데이터 컬럼 설정
            data.columns = ["산업코드","산업코드명"]
            # data 객체를 DataFrame 형식으로 변환
            df = pd.DataFrame(data)
            # index 재설정
            df.reset_index(drop=True, inplace=True)
            return df
        
        # DART코드와 주식코드를 매핑한 DataFrame 과 주식코드를 변수로 받는 getCorpCode 함수
        def getCorpCode(self, df: pd.DataFrame, stockcode:str):
            try:
            	# 입수된 DaraFrame 의 주식코드 컬럼과 입수된 주식코드가 같으면 같은 row의 DART코드 출력
                corpcode = df.loc[df["주식코드"]==stockcode,"DART코드"].values[0]        
            except (KeyError, IndexError):
            	# 에러처리 매핑한 DataFrame 에 조건에 맞는 key 나 index 가 없으면 "n/a"반환
                corpcode = "n/a"   
            return corpcode
    	# 산업코드와 산업코드명을 매핑한 DataFrame 과 산업코드를 변수로 받는 getIndustName 함수
        def getIndustName(self, df: pd.DataFrame, industcode:str):
            try:
            	# 입수된 DaraFrame 의 산업코드컬럼과 입수된 산업코드가 같으면 같은 row의 산업코드명 출력 
                industname = df.loc[df["산업코드"]==industcode, "산업코드명"].values[0]
            except (KeyError, IndexError):
            	# 에러처리 매핑한 DataFrame 에 조건에 맞는 key 나 index 가 없으면 "n/a"반환
                industname = "n/a"
            return industname

     

     

    데이터 추가 CLASS: GetAddData

    그림 1의 왼쪽에서 세 번째 클래스인 GetAddData 클래스는 종목에 대한 PER, PBR, 산업코드 정보를 입수하는 함수들이 들어있다. PER, PBR 데이터는 PER PBR 활용01 : PER PBR 데이터 입수(파이썬)에서 만든 getPerPbr 함수를 통해서 입수 가능하고, 산업코드 데이터는 PER PBR 활용04 : 주식 종목 산업분류 데이터 받아오기(DART API)에서 만든 getIndustryCode라는 함수를 통해서 입수 가능하다. getIndustryCode 함수를 실행하기 위해서는 기업공시시스템(DART)의 API(https://opendart.fss.or.kr/)를 사용해야 하기 때문에 해당 api key를 변수로 받는 클래스를 아래와 같이 만들면 된다.
     

    class GetAddData:
        def __init__(self, dartkey):
        	# dartkey 를 변수로 받고 클래스 내에서 self.apikey 라는 이름의 변수로 사용
            self.apikey = dartkey
        # getPerPbr 설정, stcode(문자열) 입수
        def getPerPbr(self, stcode:str):
        	# request 를 보낼 url 구성: 기본URL + 종목코드
            url = "https://finance.naver.com/item/main.naver?code={}".format(stcode)
            res = requests.get(url)
            # 받은 정보를 soup 에 저장
            soup = BeautifulSoup(res.text, "lxml")
            # PER, PBR 값을 입수 n/a 인 경우에는 0으로 처리
            try:
                per = float(soup.select_one("#_per").text.replace(',',''))
            except AttributeError:
                per = 0   
            try:
                pbr = float(soup.select_one("#_pbr").text.replace(',',''))
            except AttributeError:
                pbr = 0
            # PER, PBR 값을 모두 Return
            return per, pbr
            
    	# corpcode 라는 string(문자열) 변수를 받아서 산업코드 출력하는 함수
        def getIndustryCode(self, corpcode: str):
        	# URL 에 api 키와 받은 변수를 포함시켜서 응답요청
            url = "https://opendart.fss.or.kr/api/company.json"
            params = {
                "crtfc_key":self.apikey,
                "corp_code":corpcode
            }
            headers = {
                "accept": "application/json",
            }
            # 결과를 res 에 저장
            res = requests.get(url, params=params, headers=headers)
            # 응답받은 res를 json 으로 디코딩
            data = res.json()
            # 디코딩한 data를 DataFrame 형식을 df 에 저장
            df = pd.DataFrame(data, index=[0])
            # df 에서 'induty_code'컬럼을 선택하고 거기서 iloc[0]으로 내용만 입수
            try:
                industcode = str(df['induty_code'].iloc[0])
                # 표준산업분류의 소분류(세자리)만 사용하므로 네 자리 이상코드는 세 자리만 사용
                if len(industcode) >= 4:
                    industcode = industcode[:3]
            # 입수한 데이터에서 Key 나 Index를 못찾을 경우 에러 처리 "n/a"
            except (KeyError, IndexError):
                industcode = "n/a"
            return industcode

     

    SQL DB 생성 CLASS: PostDf

    그림 1의 왼쪽에서 네 번째 클래스인 PostDf 클래스는 최종적으로 생성된 데이터를 mysql 서버에 저장하는 postTable 함를 갖고 있다. mysql서버에 저장하는 함수를 쓰려면 당연히 mysql을 설치해야 한다.(SQL 기초: MySQL 중심으로 참조) mysql을 설치하면 나의 local PC에 mysql서버가 생기는데 python에서는 pymysql, sqlalchemy 등의 라이브러리를 이용하면 설치된 mysql 서버에 연결이 가능하다. 이렇게 연결된 mysql 서버에 to_sql 함수를 이용하면 내가 만든  DataFrame을 import 할 수 있다.(MYSQL에 CSV file 삽입하기: DATA IMPORT WIZARD 등 참조) postTable함수는 위에서 말한 과정 중 python과 mysql서버를 연결하고 DataFrame을  mysql 서버에 보내주는 기능을 구현한 것이다. postTable 함수 내에서 mysql 서버에 연결하기 위해서는 ID, 비밀번호, DB명 등이 필요하므로 이를 변수로 받는 클래스를 아래와 같이 만들면 된다.
     

    import pymysql
    import sqlalchemy as db
    pymysql.install_as_MySQLdb()
    
    class PostDf:
        def __init__(self, id, password, dbname):
        # id, password, dbname을 변수로 받고 클래스 내에서 self.id, self,pw, self.db 로 사용
            self.id = id
            self.pw = password
            self.db = dbname
        # postTable 함수 import 할 DataFrame 과 db에 만들 table 명을 변수로 입수
        def postTable(self, df:pd.DataFrame, tablename:str):
            hostip = '127.0.0.1' # localhost ip
            port = '3306' # mysql 서버 기본 포트
            # 로컬 서버에 id, pw, hostip, port, db name 등을 통해서 connection 주소를 생성
            db_conn_str = "mysql+mysqldb://" + self.id + ":" + self.pw + "@" + hostip + ":" + port +"/"+ self.db
            # sqlalchemy 에 create_engine 함수에 connection 문을 넣어서 db 서버와 connection
            db_conn = db.create_engine(db_conn_str,encoding='utf-8')
            # conn 인스턴스 생성
            conn = db_conn.connect()
            # 변수로 입력된 DataFrame 을 또다른 변수로 입력된 tablename이라는 table에 Import
            # 같은 테이블에 데이터가 존재하면 기존 데이터를 지우고 새로 Import
            df.to_sql(name=tablename, con=conn, if_exists='replace', index=False)
    반응형
Designed by Tistory.