[python] OpenDartReader와 marcap으로 PER/PBR/ROE등 투자지표 구하기

03_OpenDartReader_PER_PBR_ROE

OpenDartReader와 marcap으로 PER/PBR/ROE등 투자지표 구하기

1. 용어정리

1.1 PER(주가수익률; Price Earnings Ratio)

  • 주가를 1주당 순이익(EPS)로 나눈 값
$$PER=\frac{주가}{EPS}=\frac{주가}{\frac{순이익}{총발행주식수}}=\frac{주가 \times 총발행주식수}{순이익} = \frac{시가총액}{순이익}$$
  • PER이 1이면 시가총액과 기업의 순이익이 동일한 것이고, PER이 10이면 시가총액이 순이익의 10배라는 뜻

  • 보통 PER이 낮으면 주가가 기업의 이익에 비해 저평가된 것이라고 봄

  • 바이오기업이나 테크 기업 같은 경우 시장에서 기대하는 미래가치에 비해 현재의 순이익은 매우 적기 때문에 PER이 매우 크게 나타남. 그렇다고 이들 기업이 투자 가치가 떨어지는 것은 아님

1.2. PBR(주가 순자산 비율; Price Book-value Ratio)

  • 주가를 1주당 순자산가치로 나눈 값
$$PBR=\frac{주가}{\frac{순자산}{총발행주식수}}=\frac{주가 \times 총발행주식수}{순자산}=\frac{시가총액}{순자산}$$
  • PBR이 1이면 시가총액과 기업의 순자산가치가 같다는 의미

  • 보통 PBR이 낮으면 주가가 기업의 장부가치에 비해 저평가된 것이라고 봄

1.3. ROE(자기자본이익률; Return On Equity)

  • 당기순이익을 순자산(자기자본)으로 나눈 값
$$ROE=\frac{당기순이익}{순자산}=\frac{\frac{시가총액}{순자산}}{\frac{시가총액}{당기순이익}}=\frac{PBR}{PER}$$
  • PBR과 PER을 알고 있으면 ROE를 계산할 수 있음

  • ROE/PER > 3 일 경우 투자가치가 있다고 판단하는 경우도 있음(브라운스톤공식)

  • PBR이 같아도 ROE와 PER에 따라 다른 의미를 가짐

    • ROE와 PER이 각각 (20%, 5배), (5%, 20배)인 두 기업이 있으면 PBR은 1로 같지만 ROE가 높고 PER이 낮은 기업이 현재 재무구조상 더 좋다고 볼 수 있음

1.4. ROA(총자산수익률; Return On Assets)

  • 당기순이익을 자산총액으로 나눈 값
$$ROA = \frac{당기순이익}{자산총액}$$
  • 특정 기업이 자산을 얼마나 효율적으로 운영했는지 보여주는 지표

  • 일반적으로 ROE의 경우 업계 평균 ROE, 동종기업 ROE와 비교하여 기업 실적을 판단하는데 사용됨

  • 하지만 ROE로는 부채로 발생한 손익여부를 확인할 수 없기 때문에 부채 비중이 큰 업종에는 적합하지 않을 수 있음. 이 경우에는 ROE보다 ROA를 사용하는 것이 적합

2. OpenDartReader로 투자지표 계산에 필요한 정보 조회하기

  • PER을 계산하기 위해 필요한 정보: 주가, 당기순이익, 총발행주식수(보통주+우선주)

  • PBR을 계산하기 위해 필요한 정보: 주가, 순자산(자본), 총발행주식수(보통주+우선주)

  • ROE를 계산하기 위해 필요한 정보: 당기순이익, 순자산(자본)

  • ROA를 계산하기 위해 필요한 정보: 당기순이익, 자산(자본+부채)

  • 4개 지표를 계산하기 위해 필요한 정보는 주가, 당기순이익, 총발행주식수, 자본, 부채

2.1. 자본, 자산, 당기순이익 조회

  • corp: 종목코드, bsns_year: 사업연도, reprt_code: 보고서코드(1분기보고서 : 11013, 반기보고서 : 11012, 3분기보고서 : 11014, 사업보고서 : 11011)
In [53]:
import OpenDartReader
import pandas as pd
api_key = '발급받은 api key'
dart = OpenDartReader(api_key)
In [54]:
fs_2019 = dart.finstate_all(corp='005930', bsns_year='2019', fs_div='CFS', reprt_code=11011) 
fs_2020_3Q = dart.finstate_all(corp='005930', bsns_year='2020', fs_div='CFS', reprt_code=11014) 
  • 자본과 자산(자본+부채)은 재무상태표(sj_vis='BIS'), 당기순이익은 포괄손익계산서(sj_div='CIS')에서 확인할 수 있다.
In [55]:
fs_2020_3Q.loc[fs_2020_3Q['sj_div'].isin(['BS']) & fs_2020_3Q['account_id'].isin(['ifrs-full_Equity']), ]
Out[55]:
rcept_no reprt_code bsns_year corp_code sj_div sj_nm account_id account_nm account_detail thstrm_nm thstrm_amount frmtrm_nm frmtrm_amount ord thstrm_add_amount frmtrm_q_nm frmtrm_q_amount frmtrm_add_amount
52 20201116001248 11014 2020 00126380 BS 재무상태표 ifrs-full_Equity 자본총계 - 제 52 기 3분기말 276136188000000 제 51 기말 262880421000000 55 NaN NaN NaN NaN
In [56]:
fs_2020_3Q.loc[fs_2020_3Q['sj_div'].isin(['BS']) & fs_2020_3Q['account_id'].isin(['ifrs-full_Liabilities']), ]
Out[56]:
rcept_no reprt_code bsns_year corp_code sj_div sj_nm account_id account_nm account_detail thstrm_nm thstrm_amount frmtrm_nm frmtrm_amount ord thstrm_add_amount frmtrm_q_nm frmtrm_q_amount frmtrm_add_amount
42 20201116001248 11014 2020 00126380 BS 재무상태표 ifrs-full_Liabilities 부채총계 - 제 52 기 3분기말 99652554000000 제 51 기말 89684076000000 44 NaN NaN NaN NaN
In [57]:
fs_2020_3Q.loc[fs_2020_3Q['sj_div'].isin(['IS']) & fs_2020_3Q['account_id'].isin(['ifrs-full_ProfitLossAttributableToOwnersOfParent']), ]
Out[57]:
rcept_no reprt_code bsns_year corp_code sj_div sj_nm account_id account_nm account_detail thstrm_nm thstrm_amount frmtrm_nm frmtrm_amount ord thstrm_add_amount frmtrm_q_nm frmtrm_q_amount frmtrm_add_amount
68 20201116001248 11014 2020 00126380 IS 손익계산서 ifrs-full_ProfitLossAttributableToOwnersOfParent 지배기업의 소유주에게 귀속되는 당기순이익(손실) - 제 52 기 3분기 9266814000000 NaN NaN 15 19645377000000 제 51 기 3분기 6105039000000 16277059000000
In [58]:
# 자본과 부채는 재무상태표에서 당기금액('thstrm_amount') 값을 가져오면 됨
equity = int(fs_2020_3Q.loc[fs_2020_3Q['sj_div'].isin(['BS']) & fs_2020_3Q['account_id'].isin(['ifrs-full_Equity']), 'thstrm_amount'].replace(",", "")) # 당기자본(자본총계)
liability = int(fs_2020_3Q.loc[fs_2020_3Q['sj_div'].isin(['BS']) & fs_2020_3Q['account_id'].isin(['ifrs-full_Liabilities']), 'thstrm_amount'].replace(",", "")) # 당기부채(부채총계)
assets = equity + liability # 자산총계
  • 당기순이익은 '지배기업귀속'에 해당하는 값을 조회
In [61]:
# 2019 4분기 ~ 2020 3분기까지의 당기순이익의 합을 구하려면 2019년 4분기 당기순이익과 2020년 1분기 ~ 3분기 당기순이익의 합을 알아여함
# 2020년 1분기 ~ 3분기 당기순이익의 합은 2020년 3분기 손익계산서에서 'thstrm_add_amount' 값을 가져오면 되고
# 2019년 4분기 당기순이익은 2019년 전체 당기순이익에서 2019년 1분기 ~ 3분기 당기순이익의 합을 빼서 구할 수 있음
profit_2019_3Q = int(fs_2020_3Q.loc[fs_2020_3Q['sj_div'].isin(['IS']) & fs_2020_3Q['account_id'].isin(['ifrs-full_ProfitLossAttributableToOwnersOfParent']), 'frmtrm_add_amount'].replace(",", "")) # 당기순이익
profit_2019 = int(fs_2019.loc[fs_2019['sj_div'].isin(['IS']) & fs_2019['account_id'].isin(['ifrs-full_ProfitLossAttributableToOwnersOfParent']), 'thstrm_amount'].replace(",", "")) # 당기순이익
profit_2020_3Q = int(fs_2020_3Q.loc[fs_2020_3Q['sj_div'].isin(['IS']) & fs_2020_3Q['account_id'].isin(['ifrs-full_ProfitLossAttributableToOwnersOfParent']), 'thstrm_add_amount'].replace(",", "")) # 당기순이익
profit = (profit_2019-profit_2019_3Q) + profit_2020_3Q
profit
Out[61]:
24873372000000

2.2. 총발행주식수 조회

  • 사업보고서 - '소액주주'에서 보통주 총발행 주식수 확인가능
변수명 변수설명
shrholdr_co 주주수
shrholdr_tot_co 전체 주주수
shrholdr_rate 주주 비율
hold_stock_co 보유 주식수
stock_tot_co 총발행 주식수
hold_stock_rate 보유 주식 비율
In [187]:
small = dart.report('005930', '소액주주', 2020, reprt_code=11014)
small
Out[187]:
rcept_no corp_cls corp_code corp_name se shrholdr_co shrholdr_tot_co shrholdr_rate hold_stock_co stock_tot_co hold_stock_rate
0 20201116001248 Y 00126380 삼성전자 소액주주 1,754,623 1,754,776 99.99% 3,711,273,219 5,969,782,550 62.17%
In [188]:
stock_tot_co = int(small['stock_tot_co'].str.replace(',', ''))
stock_tot_co
Out[188]:
5969782550

3. marcap으로 종가와 총발행주식수 구하기

  • 문제점: EPS는 보통주와 우선주의 합인 수정평균발행주식수를 이용하여 계산하는데 Open dart API를 통해서는 우선주의 총발행 주식수를 정확하게 확인할 수 있는 방법이 없음

  • https://financedata.github.io/marcap/ 에서 제공하는 상장기업 시가총액 데이터셋 사용

변수명 변수설명
Date 날짜 (DatetimeIndex)
Code 종목코드
Name 종명이름
Open 시가
High 고가
Low 저가
Close 종가
Volume 거래량
Amount 거래대금
Changes 전일대비
ChagesRatio 전일비
Marcap 시가총액(백만원)
Stocks 상장주식수
MarcapRatio 시가총액비중(%)
ForeignShares 외국인 보유주식수
ForeignRatio 외국인 지분율(%)
Rank 시가총액 순위 (당일)
In [1]:
# git 명령으로 깃허브의 저장소를 복제(clone)
# 데이터와 데이터를 읽는데 도움이 되는 간단한 파이썬 유틸리티 함수가 포함되어 있음
# 윈도우 + 아나콘다 사용자는 conda install git 으로 git 설치 후 실행
!git clone "https://github.com/FinanceData/marcap.git" marcap
Cloning into 'marcap'...

Updating files:  64% (22/34)

Updating files:  67% (23/34)

Updating files:  70% (24/34)

Updating files:  73% (25/34)

Updating files:  76% (26/34)

Updating files:  79% (27/34)

Updating files:  82% (28/34)

Updating files:  85% (29/34)

Updating files:  88% (30/34)

Updating files:  91% (31/34)

Updating files:  94% (32/34)

Updating files:  97% (33/34)

Updating files: 100% (34/34)

Updating files: 100% (34/34), done.
In [2]:
# from marcap import marcap_data
# marcap_data 함수 안에 데이터타입 정의하는 부분에서 에러가 발생하여 직접 함수 정의 후 사용
In [41]:
from datetime import datetime
import numpy as np
import pandas as pd

def marcap_data(start, end=None, code=None):
    '''
    지정한 기간 데이터 가져오기
    :param datetime start: 시작일
    :param datetime end: 종료일 (지정하지 않으면 시작일과 동일)
    :param str code: 종목코드 (지정하지 않으면 모든 종목)
    :return: DataFrame
    '''
    start = pd.to_datetime(start)
    end = start if end==None else pd.to_datetime(end)
    df_list = []

#     dtypes={'Code':str, 'Name':str, 
#             'Open':int, 'High':int, 'Low':int, 'Close':int, 'Volume':int, 'Amount':int,
#             'Changes':int, 'ChangeCode':str, 'ChagesRatio':float, 'Marcap':int, 'Stocks':int,
#             'MarketId':str, 'Market':str, 'Dept':str,
#             'Rank':int}

# Amount, Marcap을 int 타입으로 변환할 수 없다는 에러메시지 발생

    dtypes={'Code':str, 'Name':str, 
            'Open':int, 'High':int, 'Low':int, 'Close':int, 'Volume':int, 'Amount':float,
            'Changes':int, 'ChangeCode':str, 'ChagesRatio':float, 'Marcap':float, 'Stocks':int,
            'MarketId':str, 'Market':str, 'Dept':str,
            'Rank':int}

    for year in range(start.year, end.year + 1):
        try:
            csv_file = 'marcap/data/marcap-%s.csv.gz' % (year)
            df = pd.read_csv(csv_file, dtype=dtypes, parse_dates=['Date'])
            df_list.append(df)
        except Exception as e:
            print(e)
            pass
    df_merged = pd.concat(df_list)
    df_merged = df_merged[(start <= df_merged['Date']) & (df_merged['Date'] <= end)]  
    df_merged = df_merged.sort_values(['Date','Rank'])
    if code:
        df_merged = df_merged[code == df_merged['Code']]  
    df_merged.set_index('Date', inplace=True)
    return df_merged[df_merged['Volume'] > 0]
In [86]:
# 삼성전자
df_005930 = marcap_data('2021-01-21', code='005930')
df_005930 = df_005930.assign(Amount=df_005930['Amount'].astype('int64'),
                             Marcap=df_005930['Marcap'].astype('int64'))
df_005930
Out[86]:
Code Name Market Dept Close ChangeCode Changes ChagesRatio Open High Low Volume Amount Marcap Stocks MarketId Rank
Date
2021-01-21 005930 삼성전자 KOSPI NaN 88100 1 900 1.03 87500 88600 86500 25318011 2211209788500 525937842655000 1674815254 STK 1
  • 상장주식수('Stocks') 데이터를 가져오는 코드에 문제가 있는 것으로 보임
In [87]:
df_005930['Stocks'].iat[0]
Out[87]:
1674815254
In [88]:
# 시가총액/종가 로 계산하면 맞게 나옴
stocks_005930 = int(df_005930['Marcap']/df_005930['Close'])
stocks_005930
Out[88]:
5969782550
In [89]:
# 삼성전자우
df_005935 = marcap_data('2021-01-21', code='005935')
df_005935 = df_005935.assign(Amount=df_005935['Amount'].astype('int64'),
                             Marcap=df_005935['Marcap'].astype('int64'))
df_005935
Out[89]:
Code Name Market Dept Close ChangeCode Changes ChagesRatio Open High Low Volume Amount Marcap Stocks MarketId Rank
Date
2021-01-21 005935 삼성전자우 KOSPI NaN 77600 1 800 1.04 77500 77800 76800 2512634 194360911300 63856007920000 822886700 STK 4
In [91]:
df_005935['Stocks'].iat[0]
Out[91]:
822886700
In [90]:
stocks_005935 = int(df_005935['Marcap']/df_005935['Close'])
stocks_005935
Out[90]:
822886700
  • 우선주 상장주식수는 맞게 들어가 있음
In [94]:
total_stocks = stocks_005930 + stocks_005935
total_stocks
Out[94]:
6792669250

4. 지표계산

In [101]:
EPS = profit/total_stocks
PER = df_005930['Close'].iat[0]/EPS
print('EPS: {}, PER: {}'.format(int(round(EPS)), round(PER, 2)))
EPS: 3662, PER: 24.06
In [102]:
BPS = equity/total_stocks
PBR = df_005930['Close'].iat[0]/BPS
print('BPS: {}, PBR: {}'.format(int(round(BPS)), round(PBR, 2)))
BPS: 40652, PBR: 2.17
In [105]:
ROE = PBR/PER
ROA = profit/assets
print('ROE: {}%, ROA: {}%'.format(round(ROE*100, 2), round(ROA*100, 2)))
ROE: 9.01%, ROA: 6.62%