1. 프로젝트 기간: 2023.06.12 ~

 

2. 프로젝트 개요

  - 지방행정제재·부과금의 징수 등에 관한 법률: 과징금, 이행강제금, 부담금, 변상금, 그 밖의 조세 외의 금전의 체납처분 절차를 규정

  - 이 법의 특징은 다른 개별법의 조문에서 '지방행정제재·부과금의 징수 등에 관한 법률을 준용한다'라고 규정하는 경우에 적용

  - 현재는 이 법률을 준용하는 개별법을 직접 검색하고 재·개정문을 직접 확인하는 중이라 누락 및 번거로움이 발생

  - 국가법령정보센터(현행 법령), 법제처 누리집(입법예고문), 국회 의안정보시스템(의원 발의 법률)의 실시간 관리 자동화 필요

 

3. 프로젝트 진행 과정

import pandas as pd
import numpy as np
from datetime import datetime
from datetime import timedelta
from selenium import webdriver
from selenium.webdriver.common.by import By
import time
import os
from bs4 import BeautifulSoup
import requests
import fitz
from PyQt5 import QtCore, GtGui, QtWidgets

  1) 국회 의안정보시스템 크롤링

    - 국회 의안정보시스템의 의안 검색 메뉴에서 '지방행정제재·부과금의 징수 등에 관한 법률' 검색

    - 검색 결과로 해당 법률을 포함하여 의원이 발의한 의안이 검색됨

    - 위 페이지에서 각 의원 발의 법률의 법률명, 제안일, 의결일, 의결 결과를 크롤링하여 정리

# 검색어를 검색한 페이지의 url
# PageSizeOption=100을 변수로 주어 한 페이지에 100개의 게시글까지 나타나도록 설정
url = 'https://likms.assembly.go.kr/bill/BillSearchProposalResult.do?query=지방행정제재ㆍ부과금의+징수+등에+관한+법률&PageSizeOption=100'

# BeautifulSoup를 사용해 해당 url의 html 문서 가져오기
soup = BeautifulSoup(requests.get(url).text, 'html.parser')

# 크롤링하려는 대상인 각 게시글의 제목의 클래스명을 전부 선택하여 추출
result = soup.select('p.ti a')

# 제목에서 추출하려는 대상을 리스트 형식으로 생성
법령명 = []
제안일 = []
의결일 = []
결과 = []

# 각 제목에서 법령명, 제안일, 의결일, 의결 결과를 분리하여 각 리스트에 추가
for i in range(len(result)):
	# 법령명은 제목 바로 앞의 'ㆍ'부터 '제안일'이라는 글씨가 나오기 바로 전 까지
    법령명.append(str(result[i])[str(result[i]).find('ㆍ') + 1 : str(result[i]).find('제안일')])
    # 제안일은 제안일이라는 글씨가 시작되고 6번째부터 '의결일'이라는 글씨가 시작되기 2번째전까지
    제안일.append(str(result[i])[str(result[i]).find('제안일') + 6 : str(result[i]).find('의결일') - 2])
    # 의결일은 '의결일'이라는 글씨가 시작되고 6번째부터 '의결일'이라는 글씨가 시작되고 12번째까지
    의결일.append(str(result[i])[str(result[i]).find('의결일') + 6 : str(result[i]).find('의결일') + 16])
    # 결과는 '의결일'이라는 글씨가 시작되고 18번째부터 마지막에서 5번째전까지
    결과.append(str(result[i])[str(result[i]).find('의결일') + 18 : -5])

# 각 리스트에 추가해둔 전체 데이터를 데이터프레임으로 결합
의안 = pd.DataFrame({'법령명' : 법령명, '제안일' : 제안일, '의결일' : 의결일, '결과' : 결과})

# 제안일과 의결일을 날짜 형식으로 바꾸지 위해 '.'로 연월일이 나눠져 있던 것을 '-'로 나뉘도록 변경
의안[['제안일', '의결일']] = 의안[['제안일', '의결일']].replace('.', '-')
# 제안일과 의결일을 datetime 형식으로 변경
의안['제안일'] = 의안['제안일'].apply(lambda x: pd.to_datetime(x))
# 의결일 중 의결이 아직 되지 않은 것은 </a>로 되어 있고 이는 '미정'이라는 글자로 변경
의안['의결일'] = 의안['의결일'].apply(lambda x: pd.to_datetime(x) if x != ' </a>' else '미정')

# 제안일과 의결일을 다시 str 형식으로 변경
# datetime 형식으로 두면 00:00:00이라는 시간까지 포함되어 출력되므로 str 형식으로 깔끔하게 정리
의안 = 의안.astype({'제안일':'str', '의결일':'str'})
# 의결일은 '미정'이라는 글씨를 따로 처리('미정'은 그대로 '미정', 미정이 아니면 str로 바꾼 뒤 마지막에서 9번째 자리 글자까지 추출)
의안['의결일'] = 의안['의결일'].apply(lambda x: str(x)[:-9] if x != '미정' else '미정')

# 결과도 결과가 나오지 않았다면 빈 셀로 남아있으므로 이 빈 셀을 '미정'으로 변경
의안['결과'] = 의안['결과'].apply(lambda x: '미정' if x == '' else x)

# 정리한 데이터프레임을 엑셀 파일로 저장
의안.to_excel('의안목록.xlsx', index = False)

 

  2) 법제처 누리집 크롤링

    - 법제처 누리집에서 법이 입법되기 전 예고하는 입법예고문 크롤링

    - 입법예고문은 pdf 형식으로 올라가 있으므로 pdf 형식의 입법예고문을 최신순으로 일정 개수 다운 받고 텍스트를 추출하여 '지방행정제재·부과금의 징수 등에 관한 법률'이라는 텍스트가 있는지 여부를 검색

    - 입법예고문에 키워드가 있다면 해당 입법예고문의 제목을, 그렇지 않으면 '해당없음'을 출력

# 다운받은 입법예고문을 저장할 폴더 생성 후 경로 지정
# '입법예고문'이라는 이름의 폴더가 없다면 새로 생성
if not os.path.exists("입법예고문"):
    os.mkdir("입법예고문")
# '입법예고문'폴더로 경로 지정
os.chdir("입법예고문")

# 1페이지부터 4페이지까지 크롤링
for i in range(1, 5):
	# '입법예고문' 폴더 내에 각 페이지 마다 '입법예고문_1page', '입법예고문_2page' 등 폴더 생성
    if not os.path.exists(f"입법예고문_{i}page"):
        os.mkdir(f"입법예고문_{i}page")
    # 경로는 새로 만든 폴더로 이동
    os.chdir(f"입법예고문_{i}page")
    
    # url의 currentPage 변수에 페이지 번호를 전달하여 각 페이지를 불러오도록 함
    # pageCnt 변수를 10 이상의 수로 조정해도 됨
    url = f'https://www.moleg.go.kr/lawinfo/makingList.mo?mid=a10104010000&currentPage={i}&pageCnt=10&keyField=&keyWord=&stYdFmt=&edYdFmt=&lsClsCd=&cptOfiOrgCd='
	
    # 위 url의 html 문서 불러오기
    soup = BeautifulSoup(requests.get(url).text, 'html.parser')
    
    # 입법예고문의 pdf 파일 다운로드 버튼을 눌러 다운받는 방식
    # pdf 파일 다운로드 버튼이 첫번째인 경우도 있고 두번째인 경우, 그 외 경우 등이 있어 예외처리
    try:
    	# html 문서에서 클래스명이 'wrap title' div를 찾기(각 페이지의 게시글 url)
        for href in soup.find_all('div', 'wrap title'):
        	# 각 게시글 url을 지정하여 html 문서 불러오기
            page_url = "https://www.moleg.go.kr" + href.find('a')['href'].replace("¤", '&curren')
            page_soup = BeautifulSoup(requests.get(page_url).text, 'html.parser')
            # 문서 다운로드 버튼을 찾아 버튼의 이름을 탐색
            title = page_soup.find('div', 'tb_contents').find('ul').find('a').text
            # 버튼 이름에 pdf가 포함되어 있으면 pdf 파일임을 확인하고 클릭
            if 'pdf' in title:
                title = page_soup.find('div', 'tb_contents').find('ul').find('a').text[:title.find('pdf') + 3]
                file_url = page_soup.find('div', 'tb_contents').find('ul').find('a')['href']
                # 해당 버튼을 눌러 pdf 다운로드 페이지로 이동(이동하면 자동 다운로드 됨)
                file = requests.get(file_url)
                # PyMuPDF 라이브러리를 사용하여 pdf파일의 텍스트 추출
                with open(title, 'wb') as f:
                    f.write(file.content)
                    pdf = fitz.open(title)
                    cnt = 0
                    # pdf의 각 페이지에서 키워드를 검색
                    # 없으면 -1, 있으면 개수가 출력됨
                    for page in pdf:
                        text = page.get_text()
                        result = text.find('지방행정제재ㆍ부과금의 징수 등에 관한 법률')
                        # 모든 페이지에 키워드가 없어 모든 결과가 -1이라면 각 결과를 -한 결과는
                        # pdf의 모든 페이지 개수와 동일할 것
                        cnt -= result
                # 모든 페이지에 키워드가 없어 모든 결과가 -1이라면 각 결과를 -한 결과는
                # pdf의 모든 페이지 개수와 동일할 것
                # 따라서 둘을 비교하여 각 pdf에 키워드가 있는지 확인
                # 키워드가 없으면 '해당없음'
                # 키워드가 있으면 입법예고문 pdf의 제목 출력
                if cnt == len(pdf):
                	print('해당없음')
                	# 키워드가 없으면 pdf를 삭제하는 코드이지만 오류 해결 못함
                    # os.unlink(os.getcwd() + title)
                else:
                    print(title + '에서 "지방행정제재ㆍ부과금의 징수 등에 관한 법률"이 개정됨')
			
            # pdf 파일의 다운로드 버튼 위치가 두번째인 경우 html 문서에서 찾을 버튼을 두번째 버튼으로 변경
            # 이하 동일
            elif 'pdf' in page_soup.find('div', 'tb_contents').find('ul').find_all('a')[1].text:
                title = page_soup.find('div', 'tb_contents').find('ul').find_all('a')[1].text
                title = page_soup.find('div', 'tb_contents').find('ul').find_all('a')[1].text[:title.find('pdf') + 3]
                print(title)
                file_url = page_soup.find('div', 'tb_contents').find('ul').find_all('a')[1]['href']
                file = requests.get(file_url)
                with open(title, 'wb') as f:
                    f.write(file.content)
                    pdf = fitz.open(title)
                    for page in pdf:
                        text = page.get_text()
                        result = text.find('지방행정제재ㆍ부과금의 징수 등에 관한 법률')
                        cnt -= result
                    if cnt == len(pdf):
                        print("해당없음")
                        self.molegText.append("해당없음")
                        # os.unlink(os.path(os.getcwd() + title))
                    else:
                        print(title + '에서 "지방행정제재ㆍ부과금의 징수 등에 관한 법률"이 개정됨')
                        self.molegText.append(title + '에서 "지방행정제재ㆍ부과금의 징수 등에 관한 법률"이 개정됨')
                        self.law_count += 1
            # 버튼의 위치 이외에 pdf 파일이 존재하지 않는 등의 경우에는 생략하는 것으로 예외 처리
            else:
                continue
	# 버튼의 위치 이외에 pdf 파일이 존재하지 않는 등의 경우에는 생략하는 것으로 예외 처리
    except:
        pass
	
    # 한 페이지를 끝낸 뒤 다시 상위 폴더인 '입법예고문' 폴더로 경로 지정
    os.chdir('..')
# 모든 페이지의 입법예고문을 다운받은 후에는 입법예고문의 상위 경로(입법예고문 크롤링 이전 원래의 경로)로 이동
os.chdir('..')

 

  3) 국가법령정보센터 크롤링

    - 국가법령정보센터에는 현행 법령 및 입법예고가 된 시행 예정법령의 모든 조문 검색 가능

    - 법령의 조문에 '지방행정제재·부과금의 징수 등에 관한 법률'이 있는 것을 검색하여 크롤링

    - 크롤링하려는 법령 데이터가 html 문서 내에서 나타나는 것이 아닌 서버에서 실시간으로 받아오는 것으로 BeautifulSoup로 크롤링 불가능

    - 페이지 내에 csv 파일을 다운 받을 수 있는 버튼이 있어 이 버튼을 눌러 csv 파일을 받는 것으로 동적 크롤링

# chromedriver 옵션 설정
chrome_options = webdriver.ChromeOptions()
# chromedriver를 직접 열지 않고 내부적으로 실행시키는 옵션
chrome_options.add_argument('--headless')
# chromedriver에서 다운받은 파일의 경로 지정하는 옵션(현재 경로로 지정)
chrome_options.add_experimental_option('prefs', {'download.default_directory':r'{}'.format(os.getcwd())})

# chromedriver 실행
driver = webdriver.Chrome(options = chrome_options)
# 국가법령정보센터로 접속(검색어 '지방행정제재·부과금의 징수 등에 관한 법률' 적용)
driver.get('https://www.law.go.kr/lsSc.do?menuId=1&subMenuId=15&tabMenuId=81&eventGubun=060114&query=지방행정제재·부과금의+징수+등에+관한+법률')

# 상세검색 클릭
driver.find_element(By.XPATH, r'//*[@id="dtlSch"]/a[1]').click()
# 법률 체크 클릭
driver.find_element(By.XPATH, r'//*[@id="AA1"]').click()
# 검색 클릭
driver.find_element(By.XPATH, r'//*[@id="detailLawCtDiv"]/div[3]/div[2]/a').click()
# 검색 결과 불러오는 동안 대기
time.sleep(1)
# 파일 저장 버튼 클릭
driver.find_element(By.XPATH, r'//*[@id="WideListDIV"]/div[1]/div[1]/div[3]/div[13]/button').click()
# 파일 저장형식 xls로 클릭
driver.find_element(By.XPATH, r'//*[@id="saveForm"]/div[1]/div/div[2]/a[1]').click()
# Alert 창 뜨는 동안 대기
time.sleep(1)
# Alert 창 확인 클릭
result = driver.switch_to.alert
result.accept()
# 다운 받아지는 동안 대기
time.sleep(1)

# chromedriver 종료
driver.quit()

 

  4) 크롤링 과정 통합하여 프로그램화

    - pyqt5 사용

    - 기본 틀 만들기

    - 법안의 발의 과정에 따라 의원발의 법안 → 입법예고문 → 현행 법령 순서로 크롤링 기능 배치

class Ui_Form(object):
    def setupUi(self, Form):
        Form.setObjectName("Form")
        self.gridLayout = QtWidgets.QGridLayout(Form)
        self.gridLayout.setObjectName("gridLayout")
		
        # 불러오기 버튼
        self.horizontalLayout = QtWidgets.QHBoxLayout()
        self.horizontalLayout.setObjectName("horizontalLayout")

        self.assemblyLoadBtn = QtWidgets.QPushButton(Form)
        self.assemblyLoadBtn.setObjectName("assemblyLoadBtn")
        self.horizontalLayout.addWidget(self.assemblyLoadBtn)

        self.molegLoadBtn = QtWidgets.QPushButton(Form)
        self.molegLoadBtn.setObjectName("molegLoadBtn")
        self.horizontalLayout.addWidget(self.molegLoadBtn)

        self.lawLoadBtn = QtWidgets.QPushButton(Form)
        self.lawLoadBtn.setObjectName("lawLoadBtn")
        self.horizontalLayout.addWidget(self.lawLoadBtn)

        self.gridLayout.addLayout(self.horizontalLayout, 0, 0, 1, 1)
        
		
        # 불러오기 결과
        self.horizontalLayout_2 = QtWidgets.QHBoxLayout()
        self.horizontalLayout_2.setObjectName("horizontalLayout_2")

        self.assemblyLoadResult = QtWidgets.QLineEdit(Form)
        self.assemblyLoadResult.setObjectName("assemblyLoadResult")
        self.horizontalLayout_2.addWidget(self.assemblyLoadResult)

        self.molegLoadResult = QtWidgets.QLineEdit(Form)
        self.molegLoadResult.setObjectName("molegLoadResult")
        self.horizontalLayout_2.addWidget(self.molegLoadResult)

        self.lawLoadResult = QtWidgets.QLineEdit(Form)
        self.lawLoadResult.setObjectName("lawLoadResult")
        self.horizontalLayout_2.addWidget(self.lawLoadResult)

        self.gridLayout.addLayout(self.horizontalLayout_2, 1, 0, 1, 1)
        
		
        # 결과 불러오기 버튼
        self.horizontalLayout_3 = QtWidgets.QHBoxLayout()
        self.horizontalLayout_3.setObjectName("horizontalLayout_3")

        self.assemblyCountBtn = QtWidgets.QPushButton(Form)
        self.assemblyCountBtn.setObjectName("assemblyCountBtn")
        self.horizontalLayout_3.addWidget(self.assemblyCountBtn)

        self.molegCountBtn = QtWidgets.QPushButton(Form)
        self.molegCountBtn.setObjectName("molegCountBtn")
        self.horizontalLayout_3.addWidget(self.molegCountBtn)

        self.lawCountBtn = QtWidgets.QPushButton(Form)
        self.lawCountBtn.setObjectName("lawCountBtn")
        self.horizontalLayout_3.addWidget(self.lawCountBtn)

        self.gridLayout.addLayout(self.horizontalLayout_3, 2, 0, 1, 1)
       
		
        # 결과 불러오기 결과
        self.horizontalLayout_4 = QtWidgets.QHBoxLayout()
        self.horizontalLayout_4.setObjectName("horizontalLayout_4")

        self.assemblyCountResult = QtWidgets.QTextEdit(Form)
        self.assemblyCountResult.setObjectName("assemblyCountResult")
        self.horizontalLayout_4.addWidget(self.assemblyCountResult)
        self.molegCountResult = QtWidgets.QTextEdit(Form)
        self.molegCountResult.setObjectName("molegCountResult")
        self.horizontalLayout_4.addWidget(self.molegCountResult)
        self.lawCountResult = QtWidgets.QTextEdit(Form)
        self.lawCountResult.setObjectName("lawCountResult")
        self.horizontalLayout_4.addWidget(self.lawCountResult)

        self.gridLayout.addLayout(self.horizontalLayout_4, 3, 0, 1, 1)
        
		
        # 시각화 창
        self.verticalLayout = QtWidgets.QVBoxLayout()
        self.verticalLayout.setObjectName("verticalLayout")

        self.label = QtWidgets.QLabel(Form)
        self.label.setObjectName("label")
        self.verticalLayout.addWidget(self.label)
        self.assemblyTable = QtWidgets.QTableWidget(Form)
        self.assemblyTable.setObjectName("assemblyTable")
        self.assemblyTable.setColumnCount(0)
        self.assemblyTable.setRowCount(0)
        self.verticalLayout.addWidget(self.assemblyTable)
        
        self.label_2 = QtWidgets.QLabel(Form)
        self.label_2.setObjectName("label_2")
        self.verticalLayout.addWidget(self.label_2)
        self.molegText = QtWidgets.QTextBrowser(Form)
        self.molegText.setObjectName("molegTable")
        self.verticalLayout.addWidget(self.molegText)
        
        self.label_3 = QtWidgets.QLabel(Form)
        self.label_3.setObjectName("label-3")
        self.verticalLayout.addWidget(self.label_3)
        self.lawTable = QtWidgets.QTableWidget(Form)
        self.lawTable.setObjectName("lawTable")
        self.lawTable.setColumnCount(0)
        self.lawTable.setRowCount(0)
        self.verticalLayout.addWidget(self.lawTable)

        self.gridLayout.addLayout(self.verticalLayout, 4, 0, 1, 1)

        self.retranslateUi(Form)
        QtCore.QMetaObject.connectSlotsByName(Form)

    def retranslateUi(self, Form):
        _translate = QtCore.QCoreApplication.translate
        Form.setWindowTitle(_translate("Form", "Form"))
        self.assemblyLoadBtn.setText(_translate("Form", "국회 의안정보시스템 의원발의 법률 가져오기"))
        self.molegLoadBtn.setText(_translate("Form", "법제처 누리집 입법예고 가져오기"))
        self.lawLoadBtn.setText(_translate("Form", "국가법령정보센터 법령 가져오기"))
        self.assemblyCountBtn.setText(_translate("Form", "국회 의안정보시스템 의원발의 법률 확인"))
        self.molegCountBtn.setText(_translate("Form", "법제처 누리집 입법예고 확인"))
        self.lawCountBtn.setText(_translate("Form", "국가법령정보센터 법령 확인"))
        self.label.setText(_translate("Form", "국회 의안정보시스템 의원발의 법률"))
        self.label_2.setText(_translate("Form", "법제처 누리집 입법예고"))
        self.label_3.setText(_translate("Form", "국가법령정보센터 법령"))

if __name__ == "__main__":
    import sys
    app = QtWidgets.QApplication(sys.argv)
    Form = QtWidgets.QWidget()
    ui = Ui_Form()
    ui.setupUi(Form)
    Form.show()
    sys.exit(app.exec_())

 

     - 만들어진 버튼에 각 기능 삽입

# 의안 목록 가져오기('국회 의안정보시스템 의원발의 법률 가져오기'버튼과 연결)
def assemblyLoad(self):
    self.assemblyLoadResult.setText('의안목록 가져오기 진행중')
    url = 'https://likms.assembly.go.kr/bill/BillSearchProposalResult.do?query=지방행정제재ㆍ부과금의+징수+등에+관한+법률&PageSizeOption=100'

    soup = BeautifulSoup(requests.get(url).text, 'html.parser')
    result = soup.select('p.ti a')
    법령명 = []
    제안일 = []
    의결일 = []
    결과 = []
    for i in range(len(result)):
        법령명.append(str(result[i])[str(result[i]).find('ㆍ') + 1 : str(result[i]).find('제안일')])
        제안일.append(str(result[i])[str(result[i]).find('제안일') + 6 : str(result[i]).find('의결일') - 2])
        의결일.append(str(result[i])[str(result[i]).find('의결일') + 6 : str(result[i]).find('의결일') + 16])
        결과.append(str(result[i])[str(result[i]).find('의결일') + 18 : -5])
    의안 = pd.DataFrame({'법령명' : 법령명, '제안일' : 제안일, '의결일' : 의결일, '결과' : 결과})
    의안[['제안일', '의결일']] = 의안[['제안일', '의결일']].replace('.', '-')
    의안['제안일'] = 의안['제안일'].apply(lambda x: pd.to_datetime(x))
    의안['의결일'] = 의안['의결일'].apply(lambda x: pd.to_datetime(x) if x != ' </a>' else '미정')
    의안 = 의안.astype({'제안일':'str', '의결일':'str'})
    의안['의결일'] = 의안['의결일'].apply(lambda x: str(x)[:-9] if x != '미정' else '미정')
    의안['결과'] = 의안['결과'].apply(lambda x: '미정' if x == '' else x)
    의안.to_excel('의안목록.xlsx', index = False)
    self.assemblyLoadResult.setText('의안목록 가져오기 완료')

# 가져온 의안 목록에서 개수 세기('국회 의안정보시스템 의안발의 법률 확인'버튼과 연결)
def assemblyCount(self):
    의안 = pd.read_excel('의안목록.xlsx')
    self.assemblyCountResult.setText('총 의안 개수: {}\n원안가결 의안 개수: {}\n대안반영폐기 개수: {}\n의결 전 의안 개수: {}'.format(len(의안), len(의안[의안['결과']=='원안가결']), len(의안[의안['결과'].isin(['임기만료폐기', '대안반영폐기'])]), len(의안[~의안['결과'].isin(['원안가결', '임기만료폐기', '대안반영폐기'])])))
    self.assemblyTable.setRowCount(len(의안))
    self.assemblyTable.setColumnCount(len(의안.columns))
    self.assemblyTable.setHorizontalHeaderLabels(의안.columns)
    for row_index, row in enumerate(의안.index):
        for col_index, col in enumerate(의안.columns):
            item = QtWidgets.QTableWidgetItem(의안.loc[row][col])
            self.assemblyTable.setItem(row_index, col_index, item)


# 입법예고문 가져오기('법제처 누리집 입법예고문 가져오기'버튼과 연결)
def molegLoad(self):
    if not os.path.exists("입법예고문"):
        os.mkdir("입법예고문")
    os.chdir("입법예고문")
    self.law_count = 0
    for i in range(1, 5):
        if not os.path.exists(f"입법예고문_{i}page"):
            os.mkdir(f"입법예고문_{i}page")
        os.chdir(f"입법예고문_{i}page")
        url = f'https://www.moleg.go.kr/lawinfo/makingList.mo?mid=a10104010000&currentPage={i}&pageCnt=10&keyField=&keyWord=&stYdFmt=&edYdFmt=&lsClsCd=&cptOfiOrgCd='

        soup = BeautifulSoup(requests.get(url).text, 'html.parser')
        try:
            for href in soup.find_all('div', 'wrap title'):
                page_url = "https://www.moleg.go.kr" + href.find('a')['href'].replace("¤", '&curren')
                page_soup = BeautifulSoup(requests.get(page_url).text, 'html.parser')
                title = page_soup.find('div', 'tb_contents').find('ul').find('a').text
                if 'pdf' in title:
                    title = page_soup.find('div', 'tb_contents').find('ul').find('a').text[:title.find('pdf') + 3]
                    file_url = page_soup.find('div', 'tb_contents').find('ul').find('a')['href']
                    file = requests.get(file_url)
                    with open(title, 'wb') as f:
                        f.write(file.content)
                        pdf = fitz.open(title)
                        cnt = 0
                        for page in pdf:
                            text = page.get_text()
                            result = text.find('지방행정제재ㆍ부과금의 징수 등에 관한 법률')
                            cnt -= result
                    if cnt == len(pdf):
                        self.molegText.append("해당없음")
                        # os.unlink(os.getcwd() + title)
                    else:
                        self.molegText.append(title + '에서 "지방행정제재ㆍ부과금의 징수 등에 관한 법률"이 개정됨')
                        self.law_count += 1

                elif 'pdf' in page_soup.find('div', 'tb_contents').find('ul').find_all('a')[1].text:
                    title = page_soup.find('div', 'tb_contents').find('ul').find_all('a')[1].text
                    title = page_soup.find('div', 'tb_contents').find('ul').find_all('a')[1].text[:title.find('pdf') + 3]
                    print(title)
                    file_url = page_soup.find('div', 'tb_contents').find('ul').find_all('a')[1]['href']
                    file = requests.get(file_url)
                    with open(title, 'wb') as f:
                        f.write(file.content)
                        pdf = fitz.open(title)
                        for page in pdf:
                            text = page.get_text()
                            result = text.find('지방행정제재ㆍ부과금의 징수 등에 관한 법률')
                            cnt -= result
                        if cnt == len(pdf):
                            self.molegText.append("해당없음")
                            # os.unlink(os.path(os.getcwd() + title))
                        else:
                            self.molegText.append(title + '에서 "지방행정제재ㆍ부과금의 징수 등에 관한 법률"이 개정됨')
                            self.law_count += 1
                else:
                    continue

        except:
            pass

        os.chdir('..')
    os.chdir('..')
    self.molegLoadResult.setText('의안목록 가져오기 완료')

# 입법예고문에서 키워드 포함한 예고문 개수 세기('법제처 누리집 입법예고문 확인'버튼과 연결)
def molegCount(self):
    self.molegCountResult.setText(f'관련 법률: {self.law_count}개')


# 현행 법령 가져오기('국가법령정보센터 법령 가져오기'버튼과 연결)
def lawLoad(self):
    self.lawLoadResult.setText('법령목록 가져오기 진행중')

    # chromedriver 옵션 설정
    chrome_options = webdriver.ChromeOptions()
    # chromedriver를 직접 열지 않고 내부적으로 실행시키는 옵션
    chrome_options.add_argument('--headless')
    # chromedriver에서 다운받은 파일의 경로 지정하는 옵션(현재 경로로 지정)
    chrome_options.add_experimental_option('prefs', {'download.default_directory':r'{}'.format(os.getcwd())})

    # chromedriver 실행
    driver = webdriver.Chrome(options = chrome_options)
    # 국가법령정보센터로 접속(검색어 '지방행정제재·부과금의 징수 등에 관한 법률' 적용)
    driver.get('https://www.law.go.kr/lsSc.do?menuId=1&subMenuId=15&tabMenuId=81&eventGubun=060114&query=지방행정제재·부과금의+징수+등에+관한+법률')

    # 상세검색 클릭
    driver.find_element(By.XPATH, r'//*[@id="dtlSch"]/a[1]').click()
    # 법률 체크 클릭
    driver.find_element(By.XPATH, r'//*[@id="AA1"]').click()
    # 키워드 검색
    driver.find_element(By.XPATH, r'//*[@id="lsBdyDts"]').clear()
    driver.find_element(By.XPATH, r'//*[@id="lsBdyDts"]').send_keys('지방행정제재·부과금의 징수 등에 관한 법률')
    # 검색 클릭
    driver.find_element(By.XPATH, r'//*[@id="detailLawCtDiv"]/div[3]/div[2]/a').click()
    # 검색 결과 불러오는 동안 대기
    time.sleep(1)
    # 파일 저장 버튼 클릭
    driver.find_element(By.XPATH, r'//*[@id="WideListDIV"]/div[1]/div[1]/div[3]/div[13]/button').click()
    # 파일 저장형식 xls로 클릭
    driver.find_element(By.XPATH, r'//*[@id="saveForm"]/div[1]/div/div[2]/a[1]').click()
    # Alert 창 뜨는 동안 대기
    time.sleep(1)
    # Alert 창 확인 클릭
    result = driver.switch_to.alert
    result.accept()
    # 다운 받아지는 동안 대기
    time.sleep(1)

    # chromedriver 종료
    driver.quit()

    self.lawLoadResult.setText('법령목록 가져오기 완료')

# 현행 법령 개수 세기('국가법령정보센터 법령 확인'버튼과 연결)
def lawCount(self):
    # 파일 불러오기
    법령 = pd.read_excel('법령목록.xls')
    # 중복행 제거
    법령 = 법령.drop_duplicates(subset='법령명')
    # 공포일자 및 시행일자 datetime 형식으로 변경
    법령['공포일자'] = pd.to_datetime(법령['공포일자'])
    # 시행일자가 '미정'인 것은 예정된 법령이므로 일단 현재 날짜에서 하루 더 더하기
    법령['시행일자'] = 법령['시행일자'].apply(lambda x: pd.to_datetime(x) if x != '미정' else datetime.today() + timedelta(days = 1))
    # 시행일자와 현재 일자를 비교하여 현재일자 이후에 시행되는 법령을 '예정' 법령으로 표시
    법령['예정'] = np.where((법령['시행일자'] == "미정") | (법령['시행일자'] > datetime.today()), 'o', 'x')
    법령 = 법령[['법령명', '공포일자', '시행일자', '예정']].sort_values(by = ['예정', '공포일자', '시행일자'], ascending = [True, False, False]).reset_index(drop = True)
    법령 = 법령.astype({'공포일자':'str', '시행일자':'str'})
    법령['시행일자'] = 법령['시행일자'].apply(lambda x:str(x)[:10])

    # 전체 법령 개수 출력
    self.lawCountResult.setText('총 법령 개수: {}\n시행 중인 법령 개수: {}\n시행 예정 법령 개수: {}'.format(len(법령), len(법령.loc[법령['예정'] == 'o']), len(법령.loc[법령['예정'] == 'x'])))
    # 예정된 법령을 먼저, 공포일자순으로 정렬
    # 아직 시행되지 않고 예정된 법령 확인 가능
    # 최근 업데이트된 법령 확인 가능
    self.lawTable.setRowCount(len(법령))
    self.lawTable.setColumnCount(len(법령.columns))
    self.lawTable.setHorizontalHeaderLabels(법령.columns)
    for row_index, row in enumerate(법령.index):
        for col_index, col in enumerate(법령.columns):
            item = QtWidgets.QTableWidgetItem(법령.loc[row][col])
            self.lawTable.setItem(row_index, col_index, item)

 

    - 전체 연결 코드

class Ui_Form(object):
    def setupUi(self, Form):
        Form.setObjectName("Form")
        self.gridLayout = QtWidgets.QGridLayout(Form)
        self.gridLayout.setObjectName("gridLayout")
		
        # 불러오기 버튼
        self.horizontalLayout = QtWidgets.QHBoxLayout()
        self.horizontalLayout.setObjectName("horizontalLayout")

        self.assemblyLoadBtn = QtWidgets.QPushButton(Form)
        self.assemblyLoadBtn.setObjectName("assemblyLoadBtn")
        self.horizontalLayout.addWidget(self.assemblyLoadBtn)

        self.molegLoadBtn = QtWidgets.QPushButton(Form)
        self.molegLoadBtn.setObjectName("molegLoadBtn")
        self.horizontalLayout.addWidget(self.molegLoadBtn)

        self.lawLoadBtn = QtWidgets.QPushButton(Form)
        self.lawLoadBtn.setObjectName("lawLoadBtn")
        self.horizontalLayout.addWidget(self.lawLoadBtn)

        self.gridLayout.addLayout(self.horizontalLayout, 0, 0, 1, 1)
        
		
        # 불러오기 결과
        self.horizontalLayout_2 = QtWidgets.QHBoxLayout()
        self.horizontalLayout_2.setObjectName("horizontalLayout_2")

        self.assemblyLoadResult = QtWidgets.QLineEdit(Form)
        self.assemblyLoadResult.setObjectName("assemblyLoadResult")
        self.horizontalLayout_2.addWidget(self.assemblyLoadResult)

        self.molegLoadResult = QtWidgets.QLineEdit(Form)
        self.molegLoadResult.setObjectName("molegLoadResult")
        self.horizontalLayout_2.addWidget(self.molegLoadResult)

        self.lawLoadResult = QtWidgets.QLineEdit(Form)
        self.lawLoadResult.setObjectName("lawLoadResult")
        self.horizontalLayout_2.addWidget(self.lawLoadResult)

        self.gridLayout.addLayout(self.horizontalLayout_2, 1, 0, 1, 1)
        
		
        # 결과 불러오기 버튼
        self.horizontalLayout_3 = QtWidgets.QHBoxLayout()
        self.horizontalLayout_3.setObjectName("horizontalLayout_3")

        self.assemblyCountBtn = QtWidgets.QPushButton(Form)
        self.assemblyCountBtn.setObjectName("assemblyCountBtn")
        self.horizontalLayout_3.addWidget(self.assemblyCountBtn)

        self.molegCountBtn = QtWidgets.QPushButton(Form)
        self.molegCountBtn.setObjectName("molegCountBtn")
        self.horizontalLayout_3.addWidget(self.molegCountBtn)

        self.lawCountBtn = QtWidgets.QPushButton(Form)
        self.lawCountBtn.setObjectName("lawCountBtn")
        self.horizontalLayout_3.addWidget(self.lawCountBtn)

        self.gridLayout.addLayout(self.horizontalLayout_3, 2, 0, 1, 1)
       
		
        # 결과 불러오기 결과
        self.horizontalLayout_4 = QtWidgets.QHBoxLayout()
        self.horizontalLayout_4.setObjectName("horizontalLayout_4")

        self.assemblyCountResult = QtWidgets.QTextEdit(Form)
        self.assemblyCountResult.setObjectName("assemblyCountResult")
        self.horizontalLayout_4.addWidget(self.assemblyCountResult)
        self.molegCountResult = QtWidgets.QTextEdit(Form)
        self.molegCountResult.setObjectName("molegCountResult")
        self.horizontalLayout_4.addWidget(self.molegCountResult)
        self.lawCountResult = QtWidgets.QTextEdit(Form)
        self.lawCountResult.setObjectName("lawCountResult")
        self.horizontalLayout_4.addWidget(self.lawCountResult)

        self.gridLayout.addLayout(self.horizontalLayout_4, 3, 0, 1, 1)
        
		
        # 시각화 창
        self.verticalLayout = QtWidgets.QVBoxLayout()
        self.verticalLayout.setObjectName("verticalLayout")

        self.label = QtWidgets.QLabel(Form)
        self.label.setObjectName("label")
        self.verticalLayout.addWidget(self.label)
        self.assemblyTable = QtWidgets.QTableWidget(Form)
        self.assemblyTable.setObjectName("assemblyTable")
        self.assemblyTable.setColumnCount(0)
        self.assemblyTable.setRowCount(0)
        self.verticalLayout.addWidget(self.assemblyTable)
        
        self.label_2 = QtWidgets.QLabel(Form)
        self.label_2.setObjectName("label_2")
        self.verticalLayout.addWidget(self.label_2)
        self.molegText = QtWidgets.QTextBrowser(Form)
        self.molegText.setObjectName("molegTable")
        self.verticalLayout.addWidget(self.molegText)
        
        self.label_3 = QtWidgets.QLabel(Form)
        self.label_3.setObjectName("label-3")
        self.verticalLayout.addWidget(self.label_3)
        self.lawTable = QtWidgets.QTableWidget(Form)
        self.lawTable.setObjectName("lawTable")
        self.lawTable.setColumnCount(0)
        self.lawTable.setRowCount(0)
        self.verticalLayout.addWidget(self.lawTable)

        self.gridLayout.addLayout(self.verticalLayout, 4, 0, 1, 1)

        self.retranslateUi(Form)
        QtCore.QMetaObject.connectSlotsByName(Form)

    def retranslateUi(self, Form):
        _translate = QtCore.QCoreApplication.translate
        Form.setWindowTitle(_translate("Form", "Form"))
        self.assemblyLoadBtn.setText(_translate("Form", "국회 의안정보시스템 의원발의 법률 가져오기"))
        self.molegLoadBtn.setText(_translate("Form", "법제처 누리집 입법예고 가져오기"))
        self.lawLoadBtn.setText(_translate("Form", "국가법령정보센터 법령 가져오기"))
        self.assemblyCountBtn.setText(_translate("Form", "국회 의안정보시스템 의원발의 법률 확인"))
        self.molegCountBtn.setText(_translate("Form", "법제처 누리집 입법예고 확인"))
        self.lawCountBtn.setText(_translate("Form", "국가법령정보센터 법령 확인"))
        self.label.setText(_translate("Form", "국회 의안정보시스템 의원발의 법률"))
        self.label_2.setText(_translate("Form", "법제처 누리집 입법예고"))
        self.label_3.setText(_translate("Form", "국가법령정보센터 법령"))
        
    
    # 의안 목록 가져오기('국회 의안정보시스템 의원발의 법률 가져오기'버튼과 연결)
    def assemblyLoad(self):
        self.assemblyLoadResult.setText('의안목록 가져오기 진행중')
        url = 'https://likms.assembly.go.kr/bill/BillSearchProposalResult.do?query=지방행정제재ㆍ부과금의+징수+등에+관한+법률&PageSizeOption=100'

        soup = BeautifulSoup(requests.get(url).text, 'html.parser')
        result = soup.select('p.ti a')
        법령명 = []
        제안일 = []
        의결일 = []
        결과 = []
        for i in range(len(result)):
            법령명.append(str(result[i])[str(result[i]).find('ㆍ') + 1 : str(result[i]).find('제안일')])
            제안일.append(str(result[i])[str(result[i]).find('제안일') + 6 : str(result[i]).find('의결일') - 2])
            의결일.append(str(result[i])[str(result[i]).find('의결일') + 6 : str(result[i]).find('의결일') + 16])
            결과.append(str(result[i])[str(result[i]).find('의결일') + 18 : -5])
        의안 = pd.DataFrame({'법령명' : 법령명, '제안일' : 제안일, '의결일' : 의결일, '결과' : 결과})
        의안[['제안일', '의결일']] = 의안[['제안일', '의결일']].replace('.', '-')
        의안['제안일'] = 의안['제안일'].apply(lambda x: pd.to_datetime(x))
        의안['의결일'] = 의안['의결일'].apply(lambda x: pd.to_datetime(x) if x != ' </a>' else '미정')
        의안 = 의안.astype({'제안일':'str', '의결일':'str'})
        의안['의결일'] = 의안['의결일'].apply(lambda x: str(x)[:-9] if x != '미정' else '미정')
        의안['결과'] = 의안['결과'].apply(lambda x: '미정' if x == '' else x)
        의안.to_excel('의안목록.xlsx', index = False)
        self.assemblyLoadResult.setText('의안목록 가져오기 완료')

    # 가져온 의안 목록에서 개수 세기('국회 의안정보시스템 의안발의 법률 확인'버튼과 연결)
    def assemblyCount(self):
        의안 = pd.read_excel('의안목록.xlsx')
        self.assemblyCountResult.setText('총 의안 개수: {}\n원안가결 의안 개수: {}\n대안반영폐기 개수: {}\n의결 전 의안 개수: {}'.format(len(의안), len(의안[의안['결과']=='원안가결']), len(의안[의안['결과'].isin(['임기만료폐기', '대안반영폐기'])]), len(의안[~의안['결과'].isin(['원안가결', '임기만료폐기', '대안반영폐기'])])))
        self.assemblyTable.setRowCount(len(의안))
        self.assemblyTable.setColumnCount(len(의안.columns))
        self.assemblyTable.setHorizontalHeaderLabels(의안.columns)
        for row_index, row in enumerate(의안.index):
            for col_index, col in enumerate(의안.columns):
                item = QtWidgets.QTableWidgetItem(의안.loc[row][col])
                self.assemblyTable.setItem(row_index, col_index, item)


    # 입법예고문 가져오기('법제처 누리집 입법예고문 가져오기'버튼과 연결)
    def molegLoad(self):
        if not os.path.exists("입법예고문"):
            os.mkdir("입법예고문")
        os.chdir("입법예고문")
        self.law_count = 0
        for i in range(1, 5):
            if not os.path.exists(f"입법예고문_{i}page"):
                os.mkdir(f"입법예고문_{i}page")
            os.chdir(f"입법예고문_{i}page")
            url = f'https://www.moleg.go.kr/lawinfo/makingList.mo?mid=a10104010000&currentPage={i}&pageCnt=10&keyField=&keyWord=&stYdFmt=&edYdFmt=&lsClsCd=&cptOfiOrgCd='

            soup = BeautifulSoup(requests.get(url).text, 'html.parser')
            try:
                for href in soup.find_all('div', 'wrap title'):
                    page_url = "https://www.moleg.go.kr" + href.find('a')['href'].replace("¤", '&curren')
                    page_soup = BeautifulSoup(requests.get(page_url).text, 'html.parser')
                    title = page_soup.find('div', 'tb_contents').find('ul').find('a').text
                    if 'pdf' in title:
                        title = page_soup.find('div', 'tb_contents').find('ul').find('a').text[:title.find('pdf') + 3]
                        file_url = page_soup.find('div', 'tb_contents').find('ul').find('a')['href']
                        file = requests.get(file_url)
                        with open(title, 'wb') as f:
                            f.write(file.content)
                            pdf = fitz.open(title)
                            cnt = 0
                            for page in pdf:
                                text = page.get_text()
                                result = text.find('지방행정제재ㆍ부과금의 징수 등에 관한 법률')
                                cnt -= result
                        if cnt == len(pdf):
                            self.molegText.append("해당없음")
                            # os.unlink(os.getcwd() + title)
                        else:
                            self.molegText.append(title + '에서 "지방행정제재ㆍ부과금의 징수 등에 관한 법률"이 개정됨')
                            self.law_count += 1

                    elif 'pdf' in page_soup.find('div', 'tb_contents').find('ul').find_all('a')[1].text:
                        title = page_soup.find('div', 'tb_contents').find('ul').find_all('a')[1].text
                        title = page_soup.find('div', 'tb_contents').find('ul').find_all('a')[1].text[:title.find('pdf') + 3]
                        print(title)
                        file_url = page_soup.find('div', 'tb_contents').find('ul').find_all('a')[1]['href']
                        file = requests.get(file_url)
                        with open(title, 'wb') as f:
                            f.write(file.content)
                            pdf = fitz.open(title)
                            for page in pdf:
                                text = page.get_text()
                                result = text.find('지방행정제재ㆍ부과금의 징수 등에 관한 법률')
                                cnt -= result
                            if cnt == len(pdf):
                                self.molegText.append("해당없음")
                                # os.unlink(os.path(os.getcwd() + title))
                            else:
                                self.molegText.append(title + '에서 "지방행정제재ㆍ부과금의 징수 등에 관한 법률"이 개정됨')
                                self.law_count += 1
                    else:
                        continue

            except:
                pass

            os.chdir('..')
        os.chdir('..')
        self.molegLoadResult.setText('의안목록 가져오기 완료')

    # 입법예고문에서 키워드 포함한 예고문 개수 세기('법제처 누리집 입법예고문 확인'버튼과 연결)
    def molegCount(self):
        self.molegCountResult.setText(f'관련 법률: {self.law_count}개')


    # 현행 법령 가져오기('국가법령정보센터 법령 가져오기'버튼과 연결)
    def lawLoad(self):
        self.lawLoadResult.setText('법령목록 가져오기 진행중')

        # chromedriver 옵션 설정
        chrome_options = webdriver.ChromeOptions()
        # chromedriver를 직접 열지 않고 내부적으로 실행시키는 옵션
        chrome_options.add_argument('--headless')
        # chromedriver에서 다운받은 파일의 경로 지정하는 옵션(현재 경로로 지정)
        chrome_options.add_experimental_option('prefs', {'download.default_directory':r'{}'.format(os.getcwd())})

        # chromedriver 실행
        driver = webdriver.Chrome(options = chrome_options)
        # 국가법령정보센터로 접속(검색어 '지방행정제재·부과금의 징수 등에 관한 법률' 적용)
        driver.get('https://www.law.go.kr/lsSc.do?menuId=1&subMenuId=15&tabMenuId=81&eventGubun=060114&query=지방행정제재·부과금의+징수+등에+관한+법률')

        # 상세검색 클릭
        driver.find_element(By.XPATH, r'//*[@id="dtlSch"]/a[1]').click()
        # 법률 체크 클릭
        driver.find_element(By.XPATH, r'//*[@id="AA1"]').click()
        # 키워드 검색
        driver.find_element(By.XPATH, r'//*[@id="lsBdyDts"]').clear()
        driver.find_element(By.XPATH, r'//*[@id="lsBdyDts"]').send_keys('지방행정제재·부과금의 징수 등에 관한 법률')
        # 검색 클릭
        driver.find_element(By.XPATH, r'//*[@id="detailLawCtDiv"]/div[3]/div[2]/a').click()
        # 검색 결과 불러오는 동안 대기
        time.sleep(1)
        # 파일 저장 버튼 클릭
        driver.find_element(By.XPATH, r'//*[@id="WideListDIV"]/div[1]/div[1]/div[3]/div[13]/button').click()
        # 파일 저장형식 xls로 클릭
        driver.find_element(By.XPATH, r'//*[@id="saveForm"]/div[1]/div/div[2]/a[1]').click()
        # Alert 창 뜨는 동안 대기
        time.sleep(1)
        # Alert 창 확인 클릭
        result = driver.switch_to.alert
        result.accept()
        # 다운 받아지는 동안 대기
        time.sleep(1)

        # chromedriver 종료
        driver.quit()

        self.lawLoadResult.setText('법령목록 가져오기 완료')

    # 현행 법령 개수 세기('국가법령정보센터 법령 확인'버튼과 연결)
    def lawCount(self):
        # 파일 불러오기
        법령 = pd.read_excel('법령목록.xls')
        # 중복행 제거
        법령 = 법령.drop_duplicates(subset='법령명')
        # 공포일자 및 시행일자 datetime 형식으로 변경
        법령['공포일자'] = pd.to_datetime(법령['공포일자'])
        # 시행일자가 '미정'인 것은 예정된 법령이므로 일단 현재 날짜에서 하루 더 더하기
        법령['시행일자'] = 법령['시행일자'].apply(lambda x: pd.to_datetime(x) if x != '미정' else datetime.today() + timedelta(days = 1))
        # 시행일자와 현재 일자를 비교하여 현재일자 이후에 시행되는 법령을 '예정' 법령으로 표시
        법령['예정'] = np.where((법령['시행일자'] == "미정") | (법령['시행일자'] > datetime.today()), 'o', 'x')
        법령 = 법령[['법령명', '공포일자', '시행일자', '예정']].sort_values(by = ['예정', '공포일자', '시행일자'], ascending = [True, False, False]).reset_index(drop = True)
        법령 = 법령.astype({'공포일자':'str', '시행일자':'str'})
        법령['시행일자'] = 법령['시행일자'].apply(lambda x:str(x)[:10])

        # 전체 법령 개수 출력
        self.lawCountResult.setText('총 법령 개수: {}\n시행 중인 법령 개수: {}\n시행 예정 법령 개수: {}'.format(len(법령), len(법령.loc[법령['예정'] == 'o']), len(법령.loc[법령['예정'] == 'x'])))
        # 예정된 법령을 먼저, 공포일자순으로 정렬
        # 아직 시행되지 않고 예정된 법령 확인 가능
        # 최근 업데이트된 법령 확인 가능
        self.lawTable.setRowCount(len(법령))
        self.lawTable.setColumnCount(len(법령.columns))
        self.lawTable.setHorizontalHeaderLabels(법령.columns)
        for row_index, row in enumerate(법령.index):
            for col_index, col in enumerate(법령.columns):
                item = QtWidgets.QTableWidgetItem(법령.loc[row][col])
                self.lawTable.setItem(row_index, col_index, item)

if __name__ == "__main__":
    import sys
    app = QtWidgets.QApplication(sys.argv)
    Form = QtWidgets.QWidget()
    ui = Ui_Form()
    ui.setupUi(Form)
    Form.show()
    sys.exit(app.exec_())

 

 

4. 프로젝트 결과물

    - 동작 방식

 

1. 분석 기간: 2022.11.10 ~ 2022.11.17

 

2. 분석개요

  - 전국 산부인과의 개수, 전체 의사 중 산부인과 의사 비율은 감소 추세

 

  - 산부인과 요양급여비용 심사실적, 모성사망비는 증가한 것으로 보아 산부인과의 필요성은 늘고 있음

 

  - 부산 지역을 기준으로 산부인과 127곳 중 분만실을 갖춘 곳은 32곳

  - 가임기 여성 거주지~분만실까지 최대 거리는 약 27km가 넘는 거리

  - 또한, 가임기 여성이 밀집 되어 있는 특정 아파트 단지로 부터 떨어진 분만실까지 거리는 약 23km인 곳도 존재

 

  - 이외에도 전국적으로 보건복지부에서는 33곳의 분만취약지를 선정해둠

 

  - 응급실의 경우, 분만실보다 개수는 많아 거리의 문제는 크지 않지만 다른 문제가 존재

  - 종합병원 응급실에 산부인과 의사가 상주하지 않음

  - 응급실에 찾아가도 산부인과 전문의가 응급실에 도착할 때까지 기다려야 함

  - 이에 대한 해결책: 산모 응급상황 발생 → 산부인과 전문의와 실시간 연결해주는 모바일 플랫폼을 통해 의사 호출
                                            → 스마트 이동형 산부인과 이용 또는 응급진료가 가능한 병원 이송

 

3. 플랫폼 소개

  - 실시간으로 환자와 의사를 연결해주는 모바일 플랫폼의 목업 제작

  - 산부인과 의사는 자신을 앱에 등록, 환자는 원하는 의사를 찾아 매칭

  - 긴급 상황시 매칭해주는 긴급 시스템도 탑재

 

4. 이동형 산부인과 대기 최적입지 선정 분석에 활용한 데이터

  - 가임기 여성(20대, 30대, 40대 여성) 인구수

  - 부산 지역을 기준으로 분석하여, 부산 지역 산부인과 목록 사용

  - 산부인과 목록의 병원 중 응급실, 분만실의 여부는 중앙응급의료센터 종합상황판에서 확인

 

5. 데이터 전처리

  - 분만실, 응급실, 산부인과 위치

  - 가임기 여성이 사는 거주지로부터 분만실, 응급실, 산부인과까지 최단거리

 

6. 데이터 분석

  - 산부인과 취약지수 계산(가임인구 수 + 분만실 또는 응급실까지의 최소 거리)

  - 이후 내림차순으로 정렬, 순위 도출

 

7. 분석 결론

  - 산부인과 취약지수가 높을수록 스마트 이동형 분만실이 대기하기에 적합

  - 취약지수가 높은 격자 내 또는 격자 주변에서 응급실과 분만실이 없는 산부인과를 스마트 이동형 산부인과 대기장소로 선정

  - 산부인과를 스마트 이동형 산부인과의 대기장소로 쓰는 이유는 산부인과에 상주해야 바로 산부인과 전문의와 출동할 수 있기 때문, 그렇지 않으면 산부인과나 병원에 들러 산부인과 전문의를 픽업한 뒤 거주지까지 이동해야 하므로 효율 감소

1. 분석 기간: 2022.10.15 ~ 2022.10.26

 

2. 분석개요

  - 기업의 ESG 등급이 주가에 영향을 미침

  - ESG 등급이 책정된 기업도 있지만 아직 ESG 등급이 책정되지 않은 기업도 있음

  - 이 기업들도 ESG 등급 책정을 받게 될 것이고 그 결과에 따라 기업의 주가가 영향을 받을 것임

  - 따라서 미리 기업의 ESG 등급을 예측할 수 있다면 주가 변동에 따른 이익을 얻을 수 있을 것

  - ESG 등급이 책정된 기업의 재무정보와 그에 따른 ESG 등급을 학습시켜
    ESG 등급이 책정되지 않은 기업의 재무정보를 활용해 ESG 등급을 예측

 

3. 활용 데이터

데이터명 출처 데이터 형태 비고
ESG 등급 한국ESG기준원 웹 데이터 기업코드, ESG 등급, E 등급, S 등급, G 등급
재무정보 에프앤가이드 CSV 기업코드, 기업명, 기준월, 종가, 자산총계, 부채총계, 자본총계, 매출액, 당기순이익, 배당금, EBITDA, EBIT, EV, ROE, ROA, EPS, BPS, CPS, SPS, P/E(FY End), P/B(FY End), P/C(FY End), P/CE(FY End), P/S(FY End), EV/EBITDA, EV/EBIT, EV/Sales

  - 두 데이터를 기업 코드를 기준으로 병합

 

  - ['Symbol', 'Name', '임원보수(계)', '임원보수(등기이사)', '임원보수(사외이사)', '임원1인당평균보수(계)', '임원1인당평균보수(등기이사)', '임원1인당평균보수(사외이사)', '임원스톡옵션공정가액(계)', '임원스톡옵션공정가액(등기이사)', '임원스톡옵션공정가액(사외이사)', '신용등급 (채권)', '신용등급 (CP)', '신용등급(ESB)', '신용등급 (채권, KAP)', '신용등급 (CP, KAP)', '신용등급(ESB, KAP)', '신용등급 (채권, NICE)', '신용등급 (CP, NICE)', '신용등급(ESB, NICE)', '신용등급 (채권, KIS)', '신용등급 (CP, KIS)', '신용등급(ESB, KIS)', '최대주주등 보유주식수(보통)(주)_상장협(분기)(주)', '최대주주등 보유비율(보통)(%)_상장협(분기)(%)', '최대주주등 보유주식수(우선)(주)_상장협(분기)(주)', '최대주주등 보유비율(우선)(%)_상장협(분기)(%)', '주주수 (소액주주계)_상장협(결산)', '주권의수 (소액주주계)_상장협(결산)(주)', '지분율 (소액주주계)_상장협(결산)(%)', '직원수정규직(계)(명)', '직원수정규직(남)(명)', '직원수정규직(여)(명)', '직원수비정규직(계)(명)', '직원수비정규직(남)(명)', '직원수비정규직(여)(명)', '직원평균근속년수(계)', '직원평균근속년수(남)', '직원평균근속년수(여)', '직원연간급여총액(계)', '직원연간급여총액(남)', '직원연간급여총액(여)', '직원1인평균급여액(계)', '직원1인평균급여액(남)', '직원1인평균급여액(여)']을 포함한 데이터 병합

  - 총직원수 병합

  - 시장구분코드 병합

# 위에 병합한 데이터
# 애프앤가이드 재무재표 데이터 + esg등급
# esg = pd.read_csv('/content/esg등급 합침.csv',encoding='cp949')
esg = pd.read_csv('/content/drive/MyDrive/Colab Notebooks/투자/esg등급 합침.csv',encoding='cp949')


# 임원보수 등 새로 받은 데이터
# data_oo=pd.read_csv('/content/data_ooo.csv',encoding='cp949')
data_oo=pd.read_csv('/content/drive/MyDrive/Colab Notebooks/투자/data_ooo.csv',encoding='cp949')


# 총직원수
# total=pd.read_csv('/content/총직원수.csv',encoding='cp949')
total = pd.read_csv('/content/drive/MyDrive/Colab Notebooks/투자/총직원수.csv',encoding='cp949')


# 시장구분코드
# sector=pd.read_csv('/content/csv_tb_fg_cpsm_m.csv',encoding='cp949')
sector=pd.read_csv('/content/drive/MyDrive/Colab Notebooks/투자/csv_tb_fg_cpsm_m.csv',encoding='cp949')

# 데이터 합치기
DATA=esg.join(data_oo.set_index('Symbol').iloc[:,2:],on='Code')
DATA=DATA.join(total.set_index('Code')['Annual'],on='Code')
DATA=DATA.join(sector.set_index('기업코드')['시장구분코드'],on='Code')

 

  - 분석에 사용하지 않을 열 삭제(금융을 공부한 팀원의 판단 하에 진행)

# 분석에 사용하지 않을 열 삭제
DATA = DATA.drop(['매출원가', 'Fiscal Month', '임원스톡옵션공정가액(계)', '임원스톡옵션공정가액(등기이사)',
                  '임원스톡옵션공정가액(사외이사)', '신용등급 (채권)', '신용등급 (CP)', '신용등급(ESB)', '신용등급 (채권, KAP)',
                  '신용등급 (CP, KAP)', '신용등급(ESB, KAP)', '신용등급 (채권, NICE)', '신용등급 (CP, NICE)', '신용등급(ESB, NICE)',
                  '신용등급 (채권, KIS)', '신용등급 (CP, KIS)', '신용등급(ESB, KIS)','임원1인당평균보수(계)','임원1인당평균보수(등기이사)',
                  '임원1인당평균보수(사외이사)','최대주주등 보유주식수(우선)(주)_상장협(분기)(주)','최대주주등 보유비율(우선)(%)_상장협(분기)(%)',
                  '주주수 (소액주주계)_상장협(결산)','직원연간급여총액(계)','직원연간급여총액(남)','직원연간급여총액(여)'], axis = 1)

 

4. 데이터 정제

  - 결측값, 이상값 제거

# 종가 결측값 있는 행 제거
DATA.dropna(subset=['종가'],how='any',inplace=True)

# 매출액 결측값 있는 행 제거
DATA.dropna(subset=['매출액'],how='any',inplace=True)

# EV 결측값 있는 행 제거
DATA.dropna(subset=['EV'],how='any',inplace=True)

# 인덱스 초기화
DATA.reset_index(drop=True,inplace=True)

# ROE, ROA 결측값 채우기
DATA['ROE'].fillna((DATA['당기순이익']/DATA['자본총계'])*100,inplace=True)
DATA['ROA'].fillna((DATA['당기순이익']/DATA['자산총계'])*100,inplace=True)

# 배당금 결측값 있는 행 제거
DATA.dropna(subset=['배당금'],how='any',inplace=True)

# P/CE 결측값 있는 행 제거
DATA.dropna(subset=['P/CE(FY End)'],how='any',inplace=True)

# 이상치 제거
q1 = DATA['매출액'].quantile(0.03)
q3 = DATA['매출액'].quantile(0.97)
DATA=DATA[(DATA['매출액'] < q3) & (DATA['매출액'] > q1)]

# 등기이사, 사외이사
DATA.dropna(subset=['임원보수(등기이사)'],how='any',inplace=True)
DATA['임원보수(사외이사)']=DATA['임원보수(사외이사)'].fillna(0)

# 그외 결측치 처리
DATA.dropna(subset=['직원수정규직(계)(명)'],how='any',inplace=True)
DATA.dropna(subset=['직원수정규직(남)(명)'],how='any',inplace=True)
DATA.dropna(subset=['직원수정규직(여)(명)'],how='any',inplace=True)

DATA.dropna(subset=['주권의수 (소액주주계)_상장협(결산)(주)'],how='any',inplace=True)
DATA.dropna(subset=['지분율 (소액주주계)_상장협(결산)(%)'],how='any',inplace=True)

DATA.dropna(subset=['직원평균근속년수(계)'],how='any',inplace=True)
DATA.dropna(subset=['직원평균근속년수(남)'],how='any',inplace=True)
DATA.dropna(subset=['직원평균근속년수(여)'],how='any',inplace=True)

DATA.dropna(subset=['직원1인평균급여액(계)'],how='any',inplace=True)
DATA.dropna(subset=['직원1인평균급여액(남)'],how='any',inplace=True)
DATA.dropna(subset=['직원1인평균급여액(여)'],how='any',inplace=True)

 

  - 재무제표 상에 다른 변수들로 계산된 파생변수들은 삭제

# 파생변수
# 근속년수 남녀차이
DATA['근속년수 남녀차이']=abs(DATA['직원평균근속년수(남)']-DATA['직원평균근속년수(여)'])

# 평균 급여액 남녀차이
DATA['평균급여액 남녀차이']=abs(DATA['직원1인평균급여액(남)']-DATA['직원1인평균급여액(여)'])

# 더미변수 만들기
DATA['사외이사유무']=np.where(DATA['임원보수(사외이사)'] !=0,1,0)

# 직원수비정규직 결측값 계산(총직원수-직원수정규직)
DATA['직원수비정규직(계)(명)']=DATA['직원수비정규직(계)(명)'].fillna(DATA['Annual']-DATA['직원수정규직(계)(명)'])
DATA['직원수비정규직(남)(명)']=DATA['직원수비정규직(남)(명)'].fillna(DATA['Annual']-DATA['직원수정규직(남)(명)'])
DATA['직원수비정규직(여)(명)']=DATA['직원수비정규직(여)(명)'].fillna(DATA['Annual']-DATA['직원수정규직(여)(명)'])

 

5. 데이터 분석

  - ESG 등급이 책정된 기업의 재무정보를 독립변수, ESG등급(ESG 등급, E 등급, S 등급, G 등급)을 종속변수로 분석

import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import LabelEncoder
import re
from sklearn.preprocessing import StandardScaler
from sklearn.preprocessing import MinMaxScaler
import hyperopt
from hyperopt import fmin, tpe, hp, STATUS_OK, Trials
from xgboost import XGBClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import AdaBoostClassifier
from sklearn.metrics import accuracy_score
from sklearn.metrics import mean_squared_error
from sklearn.model_selection import GridSearchCV
from sklearn.neighbors import KNeighborsClassifier
from sklearn.metrics import f1_score
from sklearn.model_selection import cross_val_predict
import warnings
warnings.filterwarnings('ignore')

# 데이터 전처리 및 데이터셋 분리
def preprocess(DATA,drop_list,pred,split):
    # ESG등급 수치화하기
    if split==1:
        ESG_CLASS={'D':0,'C':0,'B':0,'B+':1,'A':1,'A+':1}
    elif split==2:
        ESG_CLASS={'D':0,'C':1,'B':2,'B+':3,'A':4,'A+':5}
    elif split==3:
        ESG_CLASS={'D':0,'C':1,'B':2,'B+':3,'A':4,'A+':5}

    DATA['ESG등급']=DATA['ESG등급'].replace(ESG_CLASS)
    DATA['E등급']=DATA['E등급'].replace(ESG_CLASS)
    DATA['S등급']=DATA['S등급'].replace(ESG_CLASS)
    DATA['G등급']=DATA['G등급'].replace(ESG_CLASS)


    # 독립변수(X), 종속변수(y) 구분
    X=DATA.drop(drop_list,axis=1)
    y=DATA[[pred]]


    # 교차검증 위해 훈련데이터, 검증데이터 분류
    x_train,x_test,y_train,y_test=train_test_split(X,y,test_size=0.2,random_state=42)

    return x_train,x_test,y_train,y_test

# 하이퍼파라미터 서치
def search(x_train,y_train):
# adaboost, xgboost 그리드서치
    abc=AdaBoostClassifier()
    xgb=XGBClassifier(eval_metric='merror')
    lr=LogisticRegression()

    # xgboost, adaboost 파라미터 각각 서치
    # n_estimators    : 100 또는 500
    # learning_rate   : 0.01~0.5
    # algorithm       : SAMME 또는 SAMME.R
    param={'n_estimators':[100,500],'learning_rate': [(x / 100) for x in range(1, 51)]}
    grid=GridSearchCV(abc,param_grid=param,cv=3,verbose=0,n_jobs=-1)
    grid.fit(x_train,y_train)
    print('adaboost 최적 하이퍼 파라미터:',grid.best_params_)
    print('adaboost 정확도:', grid.best_score_)

    print('\n')
    # n_estimators    : 100 또는 500
    # learning_rate   : 0.01~0.5
    # max_depth       : 1~10
    param={'n_estimators':[100,500],'learning_rate': [(x / 100) for x in range(1, 51)],'max_depth':list(range(1,11))}
    grid=GridSearchCV(xgb,param_grid=param,cv=3,verbose=0,n_jobs=-1)
    grid.fit(x_train,y_train)
    print('xgboost 최적 하이퍼 파라미터:',grid.best_params_)
    print('xgboost 정확도:', grid.best_score_)

    print('\n')
    # n_estimators    : 100 또는 500
    # learning_rate   : 0.01~0.5
    # max_depth       : 1~10
    param={'C':np.logspace(-3,3,7), 'penalty':['l1', 'l2']}
    grid=GridSearchCV(lr,param_grid=param,cv=3,verbose=0,n_jobs=-1)
    grid.fit(x_train,y_train)
    print('Logistic Regression 최적 하이퍼 파라미터:',grid.best_params_)
    print('Logistic Regression 정확도:', grid.best_score_)

 

  - ESG 등급이 책정된 기업과 ESG 등급이 책정되지 않은 기업 분류 및 정규화

ESG있는거=DATA.drop(DATA.loc[DATA['ESG등급'].apply(lambda x: pd.isna(x))].index,axis=0)
ESG없는거=DATA.drop(DATA.loc[~DATA['ESG등급'].apply(lambda x: pd.isna(x))].index,axis=0).drop(['ESG등급', 'E등급', 'S등급', 'G등급'], axis=1)

# 정규화(ESG있는거에 피팅하여 ESG있는거, ESG없는거에 각각 적용)
ESG있는거_ESG=ESG있는거[['ESG등급','E등급','S등급','G등급']].reset_index()
ESG있는거=ESG있는거.drop(['ESG등급','E등급','S등급','G등급','Code','Name'],axis=1)
scaler=MinMaxScaler()
scaler.fit(ESG있는거)
ESG있는거=pd.DataFrame(scaler.transform(ESG있는거),columns=ESG있는거.columns)
ESG있는거=pd.concat([ESG있는거,ESG있는거_ESG],axis=1).drop(['index'],axis=1)

ESG없는거_code_name=ESG없는거[['Code','Name']].reset_index()
ESG없는거=ESG없는거.drop(['Code','Name'],axis=1)
ESG없는거=pd.DataFrame(scaler.transform(ESG없는거),columns=ESG없는거.columns)
ESG없는거[['Code','Name']]=ESG없는거_code_name[['Code','Name']]

 

  - 각 등급은 C, C+, B, B+, A, A+ 총 6개의 등급을 가지며 다변량 회귀분석을 진행하려 했으나,
     정확도가 0.5내외로 너무 낮은 문제 발생

  - 이에, 그룹 1(C, C+, B), 그룹2(B+, A, A+)로 나누어 로지스틱 회귀분석을 진행하고
     그룹 내에서 (C, C+)과 (B), (B+)과 (A, A+)을 다시 분류
     마지막으로 (C)와 (C+), (A)와 (A+)을 분류하여 정확도를 0.8이상으로 향상시킴

# 각각 0과 1로 구분
S_1_o=ESG있는거[(ESG있는거['S등급']=='B+') | (ESG있는거['S등급']=='A') | (ESG있는거['S등급']=='A+')]
S_0_o=ESG있는거[(ESG있는거['S등급']=='D') | (ESG있는거['S등급']=='C') | (ESG있는거['S등급']=='B')]

ESG_1_o=ESG있는거[(ESG있는거['ESG등급']=='B+') | (ESG있는거['ESG등급']=='A') | (ESG있는거['ESG등급']=='A+')]
ESG_0_o=ESG있는거[(ESG있는거['ESG등급']=='D') | (ESG있는거['ESG등급']=='C') | (ESG있는거['ESG등급']=='B')]

E_1_o=ESG있는거[(ESG있는거['E등급']=='B+') | (ESG있는거['E등급']=='A') | (ESG있는거['E등급']=='A+')]
E_0_o=ESG있는거[(ESG있는거['E등급']=='D') | (ESG있는거['E등급']=='C') | (ESG있는거['E등급']=='B')]

G_1_o=ESG있는거[(ESG있는거['G등급']=='B+') | (ESG있는거['G등급']=='A') | (ESG있는거['G등급']=='A+')]
G_0_o=ESG있는거[(ESG있는거['G등급']=='D') | (ESG있는거['G등급']=='C') | (ESG있는거['G등급']=='B')]

 

  - ESG, E, S, G 등급 서로 간의 상관을 계산
      → 다른 등급들과 관련도가 가장 높은 S 등급을 먼저 예측하여 독립변수로 두고
       기존 독립변수에 S등급까지 추가하여 ESG 등급을 예측하여 독립변수에 추가
       나머지 E, G 등급도 하나씩 예측하고 추가하며 모든 등급의 예측을 진행

  - 이때 진행한 로지스틱 회귀분석은 xgboost, randomforest, lgbm 세 알고리즘을 모두 이용한 뒤,
     각 분류 과정에서 정확도가 가장 높은 알고리즘으로 채택

# 1-1. S등급 예측(0, 1)

x_train,x_test,y_train,y_test=preprocess(ESG있는거,['ESG등급', 'E등급', 'S등급', 'G등급'],'S등급',1)
search(x_train,y_train)

# 출력 결과
adaboost 최적 하이퍼 파라미터: {'learning_rate': 0.17, 'n_estimators': 100}
adaboost 정확도: 0.804263565891473


xgboost 최적 하이퍼 파라미터: {'learning_rate': 0.33, 'max_depth': 10, 'n_estimators': 100}
xgboost 정확도: 0.8023255813953488


Logistic Regression 최적 하이퍼 파라미터: {'C': 100.0, 'penalty': 'l2'}
Logistic Regression 정확도: 0.7596899224806202
# 3개의 분류기 중 가장 정확도가 높은 adaboost에 최적 하이퍼 파라미터를 넣어 분류기 학습

abc=AdaBoostClassifier(n_estimators=100,learning_rate=0.17)
model=abc.fit(x_train,y_train)
y_cv_pred=cross_val_predict(model,x_train,y_train,cv=3)
print('accuracy=',accuracy_score(y_train,y_cv_pred))

# 출력 결과
accuracy= 0.8042635658914729
# S등급이 없는 기업의 데이터를 분류기에 넣어 S등급 예측
ESG없는거_code_name=ESG없는거[['Code','Name']].reset_index()
S등급=model.predict(ESG없는거.drop(['Code','Name'],axis=1))
ESG없는거['S등급']=S등급
ESG없는거[['Code','Name']]=ESG없는거_code_name[['Code','Name']]

S_1=ESG없는거[ESG없는거['S등급']==1].drop(['S등급'],axis=1).reset_index(drop=True)
S_0=ESG없는거[ESG없는거['S등급']==0].drop(['S등급'],axis=1).reset_index(drop=True)
S_1_code_name=S_1[['Code','Name']].reset_index()
S_0_code_name=S_0[['Code','Name']].reset_index()

 

# 1-2. S등급 중 1로 예측된 것(B+, A, A+)을 다시 세 개로 각각 분류

x_train,x_test,y_train,y_test=preprocess(S_1_o,['ESG등급', 'E등급', 'S등급', 'G등급'],'S등급',2)
search(x_train,y_train)

# 출력 결과
adaboost 최적 하이퍼 파라미터: {'learning_rate': 0.48, 'n_estimators': 500}
adaboost 정확도: 0.5569416498993963


xgboost 최적 하이퍼 파라미터: {'learning_rate': 0.03, 'max_depth': 1, 'n_estimators': 100}
xgboost 정확도: 0.5519114688128773


Logistic Regression 최적 하이퍼 파라미터: {'C': 1.0, 'penalty': 'l2'}
Logistic Regression 정확도: 0.5425217974513749
# 3개의 분류기 중 가장 정확도가 높은 adaboost에 최적 하이퍼 파라미터를 넣어 분류기 학습

abc=AdaBoostClassifier(n_estimators=500,learning_rate=0.48)
model=abc.fit(x_train,y_train)
y_cv_pred=cross_val_predict(model,x_train,y_train,cv=3)
print('accuracy=',accuracy_score(y_train,y_cv_pred))

# 출력 결과
accuracy= 0.5566037735849056
S등급=model.predict(S_1.drop(['Code','Name'],axis=1))
S_1['S등급']=S등급
S_1[['Code','Name']]=S_1_code_name[['Code','Name']]

 

# 1-3. S등급 중 0으로 예측된 것(D, C, B)을 다시 세 개로 각각 분류

x_train,x_test,y_train,y_test=preprocess(S_0_o,['ESG등급', 'E등급', 'S등급', 'G등급'],'S등급',3)
search(x_train,y_train)

# 출력 결과
adaboost 최적 하이퍼 파라미터: {'learning_rate': 0.01, 'n_estimators': 100}
adaboost 정확도: 0.6105610561056105


xgboost 최적 하이퍼 파라미터: {'learning_rate': 0.03, 'max_depth': 1, 'n_estimators': 100}
xgboost 정확도: 0.6204620462046204


Logistic Regression 최적 하이퍼 파라미터: {'C': 100.0, 'penalty': 'l2'}
Logistic Regression 정확도: 0.5907590759075907
# 3개의 분류기 중 가장 정확도가 높은 xgboost에 최적 하이퍼 파라미터를 넣어 분류기 학습

xgb=XGBClassifier(n_estimators=100,learning_rate=0.03,max_depth=1,eval_metric='merror')
model=xgb.fit(x_train,y_train)
y_cv_pred=cross_val_predict(model,x_train,y_train,cv=3)
print('accuracy=',accuracy_score(y_train,y_cv_pred))

# 출력 결과
accuracy= 0.6204620462046204
S등급=model.predict(S_0.drop(['Code','Name'],axis=1))
S_0['S등급']=S등급
S_0[['Code','Name']]=S_0_code_name[['Code','Name']]
ESG없는거=pd.concat([S_1,S_0])

 

# 2-1. S등급과 가장 상관관계가 높았던 ESG등급 예측

x_train,x_test,y_train,y_test=preprocess(ESG있는거,['ESG등급', 'E등급', 'G등급'],'ESG등급',1)
search(x_train,y_train)

# 출력 결과
adaboost 최적 하이퍼 파라미터: {'learning_rate': 0.02, 'n_estimators': 100}
adaboost 정확도: 0.8953488372093025


xgboost 최적 하이퍼 파라미터: {'learning_rate': 0.03, 'max_depth': 1, 'n_estimators': 100}
xgboost 정확도: 0.8934108527131782


Logistic Regression 최적 하이퍼 파라미터: {'C': 0.1, 'penalty': 'l2'}
Logistic Regression 정확도: 0.8953488372093025
# 3개의 분류기 중 가장 정확도가 높은 adaboost에 최적 하이퍼 파라미터를 넣어 분류기 학습

abc=AdaBoostClassifier(n_estimators=100,learning_rate=0.02)
model=abc.fit(x_train,y_train)
y_cv_pred=cross_val_predict(model,x_train,y_train,cv=3)
print('accuracy=',accuracy_score(y_train,y_cv_pred))

# 출력 결과
accuracy= 0.8953488372093024
# ESG등급이 없는 기업의 데이터를 분류기에 넣어 ESG등급 예측

ESG없는거_code_name=ESG없는거[['Code','Name']].reset_index()
ESG등급=model.predict(ESG없는거.drop(['Code','Name'],axis=1))
ESG없는거['ESG등급']=ESG등급
ESG없는거[['Code','Name']]=ESG없는거_code_name[['Code','Name']]
ESG_1=ESG없는거[ESG없는거['ESG등급']==1].drop(['ESG등급'],axis=1).reset_index(drop=True)
ESG_0=ESG없는거[ESG없는거['ESG등급']==0].drop(['ESG등급'],axis=1).reset_index(drop=True)
ESG_1_code_name=ESG_1[['Code','Name']].reset_index()
ESG_0_code_name=ESG_0[['Code','Name']].reset_index()

 

# 2-2. ESG등급 중 1로 예측된 것(B+, A, A+)을 다시 세 개로 각각 분류

x_train,x_test,y_train,y_test=preprocess(ESG_1_o,['ESG등급', 'E등급', 'G등급'],'ESG등급',2)
search(x_train,y_train)

# 출력 결과
adaboost 최적 하이퍼 파라미터: {'learning_rate': 0.14, 'n_estimators': 100}
adaboost 정확도: 0.7388888888888889


xgboost 최적 하이퍼 파라미터: {'learning_rate': 0.06, 'max_depth': 1, 'n_estimators': 100}
xgboost 정확도: 0.8277777777777778


Logistic Regression 최적 하이퍼 파라미터: {'C': 0.001, 'penalty': 'l2'}
Logistic Regression 정확도: 0.8166666666666668
# 3개의 분류기 중 가장 정확도가 높은 xgboost에 최적 하이퍼 파라미터를 넣어 분류기 학습

xgb=XGBClassifier(n_estimators=100,learning_rate=0.06, max_depth=1, eval_metric='merror')
model=xgb.fit(x_train,y_train)
y_cv_pred=cross_val_predict(model,x_train,y_train,cv=3)
print('accuracy=',accuracy_score(y_train,y_cv_pred))

# 출력 결과
accuracy= 0.8277777777777777
ESG등급=model.predict(ESG_1.drop(['Code','Name'],axis=1))
ESG_1['ESG등급']=ESG등급
ESG_1[['Code','Name']]=ESG_1_code_name[['Code','Name']]

 

# 2-3. ESG등급 중 0으로 예측된 것(D, C, B)을 다시 세 개로 각각 분류

x_train,x_test,y_train,y_test=preprocess(ESG_0_o,['ESG등급', 'E등급', 'G등급'],'ESG등급',3)
search(x_train,y_train)

# 출력 결과
adaboost 최적 하이퍼 파라미터: {'learning_rate': 0.49, 'n_estimators': 500}
adaboost 정확도: 0.6160714285714285


xgboost 최적 하이퍼 파라미터: {'learning_rate': 0.01, 'max_depth': 1, 'n_estimators': 500}
xgboost 정확도: 0.7142857142857143


Logistic Regression 최적 하이퍼 파라미터: {'C': 0.01, 'penalty': 'l2'}
Logistic Regression 정확도: 0.7113095238095238
# 3개의 분류기 중 가장 정확도가 높은 xgboost에 최적 하이퍼 파라미터를 넣어 분류기 학습

xgb=XGBClassifier(n_estimators=500,learning_rate=0.01, max_depth=1, eval_metric='merror')
model=xgb.fit(x_train,y_train)
y_cv_pred=cross_val_predict(model,x_train,y_train,cv=3)
print('accuracy=',accuracy_score(y_train,y_cv_pred))

# 출력 결과
accuracy= 0.7142857142857143
ESG등급=model.predict(ESG_0.drop(['Code','Name'],axis=1))
ESG_0['ESG등급']=ESG등급
ESG_0[['Code','Name']]=ESG_0_code_name[['Code','Name']]
ESG없는거=pd.concat([ESG_1,ESG_0])

 

# 3-1. ESG등급과 S등급을 제외하고 가장 상관관계 높았던 E등급 예측

x_train,x_test,y_train,y_test=preprocess(ESG있는거,['E등급', 'G등급'],'E등급',1)
search(x_train,y_train)

# 출력 결과
adaboost 최적 하이퍼 파라미터: {'learning_rate': 0.01, 'n_estimators': 500}
adaboost 정확도: 0.8643410852713179


xgboost 최적 하이퍼 파라미터: {'learning_rate': 0.26, 'max_depth': 4, 'n_estimators': 100}
xgboost 정확도: 0.8740310077519379


Logistic Regression 최적 하이퍼 파라미터: {'C': 1.0, 'penalty': 'l2'}
Logistic Regression 정확도: 0.881782945736434
# 3개의 분류기 중 가장 정확도가 높은 Logistic Regression에 최적 하이퍼 파라미터를 넣어 분류기 학습

lr=LogisticRegression(C=1,penalty='l2')
model=lr.fit(x_train,y_train)
y_cv_pred=cross_val_predict(model,x_train,y_train,cv=3)
print('accuracy=',accuracy_score(y_train,y_cv_pred))

# 출력 결과
accuracy= 0.8817829457364341
# E등급이 없는 기업의 데이터를 분류기에 넣어 E등급 예측

ESG없는거_code_name=ESG없는거[['Code','Name']].reset_index()
E등급=model.predict(ESG없는거.drop(['Code','Name'],axis=1))
ESG없는거['E등급']=E등급
ESG없는거[['Code','Name']]=ESG없는거_code_name[['Code','Name']]
E_1=ESG없는거[ESG없는거['E등급']==1].drop(['E등급'],axis=1).reset_index(drop=True)
E_0=ESG없는거[ESG없는거['E등급']==0].drop(['E등급'],axis=1).reset_index(drop=True)
E_1_code_name=E_1[['Code','Name']].reset_index()
E_0_code_name=E_0[['Code','Name']].reset_index()

 

# 3-2. E등급 중 1로 예측된 것(B+, A, A+)을 다시 세 개로 각각 분류

x_train,x_test,y_train,y_test=preprocess(E_1_o,['E등급', 'G등급'],'E등급',2)
search(x_train,y_train)

# 출력 결과
adaboost 최적 하이퍼 파라미터: {'learning_rate': 0.35, 'n_estimators': 500}
adaboost 정확도: 0.626984126984127


xgboost 최적 하이퍼 파라미터: {'learning_rate': 0.42, 'max_depth': 5, 'n_estimators': 500}
xgboost 정확도: 0.7142857142857143


Logistic Regression 최적 하이퍼 파라미터: {'C': 0.1, 'penalty': 'l2'}
Logistic Regression 정확도: 0.746031746031746
# 3개의 분류기 중 가장 정확도가 높은 Logistic Regression에 최적 하이퍼 파라미터를 넣어 분류기 학습

lr=LogisticRegression(C=0.1,penalty='l2')
model=lr.fit(x_train,y_train)
y_cv_pred=cross_val_predict(model,x_train,y_train,cv=3)
print('accuracy=',accuracy_score(y_train,y_cv_pred))

# 출력 결과
accuracy= 0.746031746031746
E등급=model.predict(E_1.drop(['Code','Name'],axis=1))
E_1['E등급']=E등급
E_1[['Code','Name']]=E_1_code_name[['Code','Name']]

 

# 3-3. E등급 중 0으로 예측된 것(D, C, B)을 다시 세 개로 각각 분류

x_train,x_test,y_train,y_test=preprocess(E_0_o,['E등급', 'G등급'],'E등급',3)
search(x_train,y_train)

# 출력 결과
adaboost 최적 하이퍼 파라미터: {'learning_rate': 0.04, 'n_estimators': 100}
adaboost 정확도: 0.6118067978533095


xgboost 최적 하이퍼 파라미터: {'learning_rate': 0.15, 'max_depth': 9, 'n_estimators': 100}
xgboost 정확도: 0.6143907771814748


Logistic Regression 최적 하이퍼 파라미터: {'C': 10.0, 'penalty': 'l2'}
Logistic Regression 정확도: 0.6068177300735439
# 3개의 분류기 중 가장 정확도가 높은 xgboost에 최적 하이퍼 파라미터를 넣어 분류기 학습

xgb=XGBClassifier(n_estimators=100,learning_rate=0.15, max_depth=9, eval_metric='merror')
model=xgb.fit(x_train,y_train)
y_cv_pred=cross_val_predict(model,x_train,y_train,cv=3)
print('accuracy=',accuracy_score(y_train,y_cv_pred))

# 출력 결과
accuracy= 0.6143958868894601
E등급=model.predict(E_0[x_train.columns])
E_0['E등급']=E등급
E_0[['Code','Name']]=E_0_code_name[['Code','Name']]
ESG없는거=pd.concat([E_1,E_0])

 

# 4-1. 마지막 남은 G등급 예측

x_train,x_test,y_train,y_test=preprocess(ESG있는거,['G등급'],'G등급',1)
search(x_train,y_train)

# 출력 결과
adaboost 최적 하이퍼 파라미터: {'learning_rate': 0.01, 'n_estimators': 500}
adaboost 정확도: 0.686046511627907


xgboost 최적 하이퍼 파라미터: {'learning_rate': 0.36, 'max_depth': 5, 'n_estimators': 100}
xgboost 정확도: 0.7073643410852712


Logistic Regression 최적 하이퍼 파라미터: {'C': 1.0, 'penalty': 'l2'}
Logistic Regression 정확도: 0.6976744186046512
# 3개의 분류기 중 가장 정확도가 높은 xgboost에 최적 하이퍼 파라미터를 넣어 분류기 학습

xgb=XGBClassifier(n_estimators=100,learning_rate=0.36,max_depth=5,eval_metric='merror')
model=xgb.fit(x_train,y_train)
y_cv_pred=cross_val_predict(model,x_train,y_train,cv=3)
print('accuracy=',accuracy_score(y_train,y_cv_pred))

# 출력 결과
accuracy= 0.7073643410852714
# G등급이 없는 기업의 데이터를 분류기에 넣어 G등급 예측

ESG없는거_code_name=ESG없는거[['Code','Name']].reset_index()
G등급=model.predict(ESG없는거[x_train.columns])
ESG없는거['G등급']=G등급
ESG없는거[['Code','Name']]=ESG없는거_code_name[['Code','Name']]
G_1=ESG없는거[ESG없는거['G등급']==1].drop(['G등급'],axis=1).reset_index(drop=True)
G_0=ESG없는거[ESG없는거['G등급']==0].drop(['G등급'],axis=1).reset_index(drop=True)
G_1_code_name=G_1[['Code','Name']].reset_index()
G_0_code_name=G_0[['Code','Name']].reset_index()

 

# 4-2. G등급 중 1로 예측된 것(B+, A, A+)을 다시 세 개로 분류

x_train,x_test,y_train,y_test=preprocess(G_1_o,['G등급'],'G등급',2)
search(x_train,y_train)

# 출력 결과
adaboost 최적 하이퍼 파라미터: {'learning_rate': 0.01, 'n_estimators': 100}
adaboost 정확도: 0.8184981684981686


xgboost 최적 하이퍼 파라미터: {'learning_rate': 0.01, 'max_depth': 1, 'n_estimators': 100}
xgboost 정확도: 0.8886752136752136


Logistic Regression 최적 하이퍼 파라미터: {'C': 0.1, 'penalty': 'l2'}
Logistic Regression 정확도: 0.8791514041514041
# 3개의 분류기 중 가장 정확도가 높은 xgboost에 최적 하이퍼 파라미터를 넣어 분류기 학습

xgb=XGBClassifier(n_estimators=100,learning_rate=0.01,max_depth=1,eval_metric='merror')
model=xgb.fit(x_train,y_train)
y_cv_pred=cross_val_predict(model,x_train,y_train,cv=3)
print('accuracy=',accuracy_score(y_train,y_cv_pred))

# 출력 결과
accuracy= 0.8885350318471338
G등급=model.predict(G_1[x_train.columns])
G_1['G등급']=G등급
G_1[['Code','Name']]=G_1_code_name[['Code','Name']]

 

# 4-3. G등급 중 0으로 예측된 것(D, C, B)을 다시 세 개로 각각 분류

x_train,x_test,y_train,y_test=preprocess(G_0_o,['G등급'],'G등급',3)
search(x_train,y_train)

# 출력 결과
adaboost 최적 하이퍼 파라미터: {'learning_rate': 0.07, 'n_estimators': 100}
adaboost 정확도: 0.8159203980099502


xgboost 최적 하이퍼 파라미터: {'learning_rate': 0.02, 'max_depth': 5, 'n_estimators': 100}
xgboost 정확도: 0.8258706467661692


Logistic Regression 최적 하이퍼 파라미터: {'C': 1.0, 'penalty': 'l2'}
Logistic Regression 정확도: 0.8308457711442786
# 3개의 분류기 중 가장 정확도가 높은 Logistic Regression에 최적 하이퍼 파라미터를 넣어 분류기 학습

lr=LogisticRegression(C=1,penalty='l2')
model=lr.fit(x_train,y_train)
y_cv_pred=cross_val_predict(model,x_train,y_train,cv=3)
print('accuracy=',accuracy_score(y_train,y_cv_pred))

# 출력 결과
accuracy= 0.8308457711442786
G등급=model.predict(G_0.drop(['Code','Name'],axis=1))
G_0['G등급']=G등급
G_0[['Code','Name']]=G_0_code_name[['Code','Name']]
ESG없는거=pd.concat([G_1,G_0])

 

# 5. 결과 저장

ESG_CLASS={0:'D',1:'C',2:'B',3:'B+',4:'A',5:'A+'}
ESG없는거['ESG등급']=ESG없는거['ESG등급'].replace(ESG_CLASS)
ESG없는거['E등급']=ESG없는거['E등급'].replace(ESG_CLASS)
ESG없는거['S등급']=ESG없는거['S등급'].replace(ESG_CLASS)
ESG없는거['G등급']=ESG없는거['G등급'].replace(ESG_CLASS)

ESG없는거=ESG없는거[['Code', 'Name', '종가', '자산총계', '부채총계', '자본총계', '매출액', '당기순이익', '배당금', 'EBITDA', 'EBIT',
                    'EV', 'ROE', 'ROA', 'EPS', 'BPS', 'CPS', 'SPS', 'P/E(FY End)', 'P/B(FY End)', 'P/C(FY End)', 'P/CE(FY End)', 'P/S(FY End)',
                    'EV/EBITDA', 'EV/EBIT', 'EV/Sales', '임원보수(등기이사)', '임원보수(사외이사)', '최대주주등 보유주식수(보통)(주)_상장협(분기)(주)',
                    '최대주주등 보유비율(보통)(%)_상장협(분기)(%)', '주권의수 (소액주주계)_상장협(결산)(주)', '지분율 (소액주주계)_상장협(결산)(%)',
                    '직원수정규직(계)(명)', '직원수정규직(남)(명)', '직원수정규직(여)(명)', '직원수비정규직(계)(명)', '직원수비정규직(남)(명)',
                    '직원수비정규직(여)(명)', '직원평균근속년수(계)', '직원평균근속년수(남)', '직원평균근속년수(여)', '직원1인평균급여액(계)',
                    '직원1인평균급여액(남)', '직원1인평균급여액(여)', 'Annual', '시장구분코드', '근속년수 남녀차이', '평균급여액 남녀차이',
                    '사외이사유무', 'ESG등급', 'E등급', 'S등급', 'G등급']]

ESG없는거.to_csv('결과.csv',encoding='cp949',index=False)

 

6. 분석 결과

  - ESG 등급이 책정된 기업들을 train data와 test data로 분리하여 train data로 학습을 진행한 뒤 test data에 적용한 결과,
     정확도가 0.8 이상인 알고리즘을 ESG 등급이 책정되지 않은 기업의 ESG 등급을 책정하는데 사용

 

7. 피드백 및 한계점

  - ESG 등급 자체는 재무정보와 아무 관련이 없어 재무정보를 통해 ESG 등급을 예측하는 것은 의미가 없음

  - 하지만, 그 사이에 연관 관계를 알고리즘을 통해 찾으려 했고 정확도를 0.8 이상으로 올리기는 하였지만, 그 결과가 유의미 할 지는 실제 ESG 등급이 나오기 까지는 알 수 없음

  - 기존 논문들은 ESG 등급과 주가 간의 상관관계만을 얘기했지만 이번 분석에서 재무정보를 ESG 등급 예측에 이용했다는 의의가 있음

 

+ Recent posts