데이터 분석에서 중요한 것 중 하나가 데이터 시각화이다. 분석한 결과를 잘 시각화하여 전달하여 다른 사람들도 내가 분석한 데이터에 대해 알게 하는 것이 데이터 분석의 마무리이다. 그렇기에 데이터 시각화를 공부해야겠다고 생각했고, 마침 태블로를 활용한 데이터 시각화를 공부할 수 있는 책을 리뷰할 수 있는 기회가 생겼다. 태블로는 PowerBI와 함께 기업에서 많이 활용하고 있는 데이터 시각화 툴 중 하나로 알고 있다.

 리뷰할 책은 이 책이다.

목차는 다음과 같다.

머리말
이 책의 구성과 특징

PART 01 태블로와 함께하는 데이터 시각화 첫걸음
CHAPTER 01 데이터 시각화는 무엇인가요?
01 데이터 시각화의 개념
02 데이터 시각화의 필요성과 역할
03 데이터 시각화의 전략

CHAPTER 02 태블로는 어떤 도구인가요?
04 태블로 제품군에 대한 이해
05 태블로 데스크탑 설치
06 태블로 인터페이스 소개
07 태블로 vs. 엑셀

PART 02 태블로와 함께하는 데이터 시각화, 차트 그리기
CHAPTER 03 데이터 시각화 기본 차트, 무작정 따라 그리기 (1)
08 필드를 시트 위에 올리는 방법
09 데이터 시각화 기본 차트 #1 - 바 차트
10 마크 카드 활용하기
11 데이터 시각화 기본 차트 #2 - 라인 차트
12 라인 차트 vs. 영역 차트
13 데이터 시각화 기본 차트 #3 - 파이 차트
14 파이 차트는 좋은 데이터 시각화 방식이 아니다?
연습 문제

CHAPTER 04 데이터 시각화 기본 차트, 무작정 따라 그리기 (2)
15 상관관계를 시각화하는 차트, 스캐터 플롯
16 추세선 활용하기
17 분포를 시각화하는 차트 #1 - 박스 플롯
18 분포를 시각화하는 차트 #2 - 히스토그램
19 테이블을 직관적으로 표현하는 방식, 하이라이트 테이블
연습 문제

CHAPTER 05 태블로 이해를 위한 열쇠, 핵심 개념 집중 공략
20 태블로 핵심 개념 #1 - 차원과 측정값
21 태블로 핵심 개념 #2 - 연속형과 불연속형
22 태블로 핵심 개념 #3 - Level of Detail
23 태블로의 4가지 계산 방법

CHAPTER 06 데이터 시각화 기본 차트 되짚어 보기

PART 03 태블로와 함께하는 데이터 시각적 분석
CHAPTER 07 시각화 목표를 향한 첫걸음, 데이터 이어 붙이기
24 태블로 데스크탑에서 데이터 결합 작업을?
25 세로로 길쭉하게 결합하는 과정, 유니온
26 가로로 빵빵하게 결합하는 과정 - 조인, 관계, 블렌딩

CHAPTER 08 필터를 활용하여 세부적으로 들여다보기
27 필터에 대한 기본 이해
28 태블로에서 필터는 왜 그렇게 중요한가?
29 추출 데이터의 범위를 조정하는 추출 필터
30 분석 데이터의 범위를 조정하는 데이터 원본 필터
31 데이터 탐색에서 가장 많이 활용되는 차원 필터
32 적용 방식에 따라 다른 의미를 가지는 측정값 필터
33 작동 우선순위를 바꾸는 필터 #1 - 컨텍스트 필터
34 작동 우선순위를 바꾸는 필터 #2 - 테이블 계산 필터와 숨기기
연습 문제

CHAPTER 09 이중 축을 활용하여 비교 맥락 만들기
35 이중 축의 정의, 2개의 축
36 이중 축의 가장 중요한 의미, 비교 맥락 만들기
37 이중 축의 추가적인 활용법, 디자인 요소 더하기
38 이중 축 vs. 결합 축
39 이중 축을 절묘하게 활용하는 도넛 차트 연습 문제

CHAPTER 10 분석 패널을 활용한 데이터 시각화 고도화
40 분석 패널 소개
41 데이터의 의미를 명확히 드러내는 보조선
42 알고리즘을 활용하는 분석 기능
연습 문제

CHAPTER 11 공간적 맥락을 드러내는 지도 시각화
43 태블로와 지도 시각화
44 지도 시각화, 양날의 검
45 지리경계 구역을 표현하는 지도 시각화
46 위치를 정확히 짚어주는 지도 시각화
연습 문제

CHAPTER 12 데이터 시각적 분석 되짚어 보기

PART 04 태블로로 완성하는 정보 종합 상황판, 대시보드
CHAPTER 13 대시보드는 시트의 조합, 그 이상이다
47 대시보드 구성 요소 - 시트, 개체, 동작
48 대시보드 콘텐츠 구성, 육하원칙(5W1H)을 따라가자
49 대시보드 실습을 위한 차트 준비
50 대시보드 인터페이스
51 대시보드 구성 방법 - 바둑판식 vs. 부동
52 시트, 개체, 동작을 활용한 대시보드 구성
53 태블로를 파워포인트처럼 활용하는 스토리

CHAPTER 14 대시보드 되짚어 보기

CHAPTER 15 기본 차트를 넘어 더 넓은 시각화의 영역으로
54 바 차트가 지루할 땐 변형을 생각해보세요
55 날짜 및 시간에 따른 변화를 표현하는 다른 방법들
56 간트 차트의 다양한 활용법

 

목차에서 알수 있듯이, 엄청 많은 내용을 책에 담고 있다. 책이 두꺼워 보이지 않았는데 넘기다보니 끝없이 많은 내용이 펼쳐졌고 태블로를 활용한 시각화에 대한 거의 모든걸 알 수 있을 것 같은 느낌이었다.

 

 책 내용은 데이터 시각화에 대한 이론과 태블로에 대한 설명으로 시작한다. 이 책의 실습 내용은 태블로 무료 버전을 활용하는 것으로, 누구나 따라하며 공부할 수 있다.

 

 데이터 시각화는 이론 공부가 아니라 직접 해보는 실습이 주요하다고 생각하는데 이런 연습 문제가 챕터마다 있어 직접 배웠던 내용을 따라하고, 응용해볼 수 있어 더 기억에 잘 남고 손에 익어 시각화 방법을 더 잘 활용할 수 있게 될 것 같다.

 

 내가 왼쪽의 단순한 라인 차트를 색깔과 요소들을 잘 조합해 오른쪽과 같은 그래프로 만들 수 있게 되다니 믿기지 않았다.

 

 이책으로 공부하면 전체적으로 여러가지 다양한 그래프들을 접해보며 시각화에 대한 감각도 기를 수 있고, 태블로 활용법도 잘 숙지하여 분석 결과를 원하는대로 시각화할 수 있을 것 같다.

 '빅데이터 분석기사' 시험의 실기를 준비하는데 큰 도움이 되는 영진닷컴에서 나온 '2023 이기적 빅데이터분석기사 실기 기본서'를 받아보게 되었다.

 필답형 + 파이썬 분석 + R 분석 3개로 구성되어 있지만 필답형은 최근 시험부터 없어졌고, 파이썬 또는 R 중 자신이 시험 칠 과목에 맞게 골라보면 되어 한 권으로 끝낼 수 있어 편해 보였다.

 필답형 대신 생긴 3유형은 이기적 카페에서 자료를 받아볼 수 있다고 한다.

 

이기적 스터디 카페_컴활,정보처리,워... : 네이버 카페

이렇게 기막힌 적중률! 이기적 자격증(컴활,정보처리,데이터,워드,ITQ,GTQ등) 온라인 스터디 카페!

cafe.naver.com

 

 실기 시험에 대한 안내는 책의 맨 앞 장에 나와있으며, 동영상 재생  목록, 질문답변, 추가 자료, 정오표, 유튜브 채널 등도 QR코드로 간편하게 찾아볼 수 있다.

 

 실기 시험 환경을 체험해 볼 수 있는 사이트도 제시해주며, 해당 사이트에 들어가서 공부하면 실긱 시험장에 가서 당황하지 않을 수 있다.

 

구름HOME

구름은 클라우드 기술을 이용하여 누구나 코딩을 배우고, 실력을 평가하고, 소프트웨어를 개발할 수 있는 클라우드 소프트웨어 생태계입니다.

www.goorm.io

 

 목차는 다음과 같이 구성되어 있다.

 

 파이썬 과목에서 파이썬 코드의 기초부터 자세히 알려줘 파이썬을 처음 해보는 사람들도 천천히 따라하면 누구든 할 수 있어 보였고, 머신러닝 전체 코드와 사용한 머신러닝의 계산 원리까지 설명해줘 이해가 쉬워보였다.

 

 R 과목도 마찬가지로 기초부터, 머신러닝 코드까지 친절하게 설명되어 있어 공부하는데 어려움을 느낄 일이 없어보였다.

 

빅데이터 분석기사 실기 시험을 준비하는 사람이 "이기적 빅데이터 분석기사 실기 기본서"와 함께 공부하면 시험에 많은 도움을 받을 수 있을 것 같다.

 파이썬을 사용하여 딥러닝을 구현할 때 사용하는 대표적인 라이브러리로 케라스, 탠서플로우, 파이토치가 있다. 케라스와 탠서플로우를 활용하는 방법은 공부해봤지만 파이토치는 아직 해본 적이 없었다. 맨날 미루고만 있다가 이 책을 발견하여 파이토치를 공부해야겠다는 마음의 불씨가 다시 한 번 피어올랐다.

 파이토치를 활용한 딥러닝 모델을 구현하고, 그 모델을 웹 또는 앱으로 개발할 수 있는 라이브러리까지 설명하고 있는 이 책은, 머신러닝과 딥러닝 모델을 구축하고 그 결과를 대시보드로 만들어 관리하고 싶은 나의 목표에 정말 잘 어울리는 책이었다.

 목차는 위와 같이 이루어져 있다.

 개발 환경에 대해 먼저 설명하고, pyotrch와 함께 딥러닝의 기본 개념에 대한 학습한다. 다음으로, CNN, RNN 등 대표적인 딥러닝 알고리즘을 공부하고 파이토치로 구현까지 같이 해 볼 수 있었다. 마지막으로, Streamlit으로 앱을 개발하고, 앱을 깃허브에 등록하여 배포하는 과정까지 살펴볼 수 있었다.

 각 내용에 맞는 코드와 그 코드 실행 결과까지 책에 나와서 실습을 하면서도 내가 맞게 하고 있는 것인지 알기 쉬웠고, 책을 따라가기 쉬웠다.

 또한, 구체적인 예제로 공부의 흥미도 더 돋울 수 있었다.

 Streamlit은 처음 접해보는 웹 앱 개발 프레임워크였지만, 책의 내용을 따라하며 배우다보니 괜찮은 프레임워크였다. 다음 프로젝트는 Streamlit을 사용하여 구축해봐도 될 것 같다.

● 책 '파이썬 머신러닝 완벽 가이드' 참고

● 예시로 '월간 데이콘 쇼츠 - 초전도체 임계온도 예측 AI 헤커톤'의 데이터 사용(train, test, submission 파일 3개 존재)

 

1. 모듈 임포트

import os
# Seed 고정
def seed_everything(seed):
  random.seed(seed)
  os.environ['PYTHONHASHSEED'] = str(seed)
  np.random.seed(seed)
seed_everything(42)
# warning 무시
import warnings
warnings.filterwarnings("ignore")

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns

from sklearn.preprocessing import LabelEncoder, OneHotEncoder
from sklearn.preprocessing import StandardScaler, MinMaxScaler

from sklearn.ensemble import RandomForestClassifier, RandomForestRegressor
from sklearn.tree import DecisionTreeClassifier, DicisionTreeRegressor
from sklearn.linear_model import LogisticRegression, LinearRegression

from sklearn.model_selection import KFold
from sklearn.model_selection import cross_val_score
from sklearn.model_selection import GridSearchCV

from sklearn.metrics import accuracy_score, mean_squared_error, mean_absolute_error, r2_score

 

 

2. 데이터 불러오기

train = pd.read_csv('train.csv')
test = pd.read_csv('test.csv')
submission = pd.read_csv('sample_submission.csv')

 

 

3. 데이터 탐색

train.head()  # 처음 5개의 데이터 (전체적인 데이터 구조 확인)
train.info()  # 데이터에 대한 개략적인 정보 (행과 열 개수, non-null값 개수, 데이터 타입)
train.describe()  # 숫자형 데이터 요약 (개수, 평균, 표준편차, 최소값, 25%, 50%, 75%, 최대값)
train.isna().sum()  # 각 데이터의 결측값 개수
# 이외에도 이해가 가지 않는 행이나 열만 따로 뽑아서 탐색, 데이터 도메인 지식 등을 찾아보며 데이터에 대한 이해 필요
train = train.drop('ID', axis = 1)  # train 데이터 탐색 결과 단순 식별자 변수인 ID는 필요 없는 것으로 판단, 삭제
feature = train.copy()  # 밑에서 target변수를 제거하고 feature 변수만 남긴 데이터
target = feature.pop('critical_temp') # target 데이터 추출, train에는 feature 데이터만 남음

  - train.head(): 처음 5개의 데이터 (전체적인 데이터 구조 확인)

  - train.info(): 데이터에 대한 개략적인 정보 (행과 열 개수, non-null값 개수, 데이터 타입)

  - train.describe(): 숫자형 데이터 요약 (개수, 평균, 표준편차, 최소값, 25%, 50%, 75%, 최대값)

  - train.isna().sum(): 각 데이터의 결측값 개수

 

 

4. 데이터 시각화

  - 박스플롯으로 이상값 등 데이터 분포 확인

scaler = StandardScaler() # 박스플롯 출력 전 하나의 박스플롯에서 데이터를 한눈에 보기 위해 같은 범위로 임시 스케일링
plt.figure(figsize = (15, 10))
sns.boxplot(scaler.fit_transform(feature))

 

  - 각 feature 데이터의 바이오린 플롯과 히스토그램 출력하여 데이터 분포 확인

for col in train.columns:
  fig, axs = plt.subplots(nrows = 1, ncols = 2, figsize = (10, 4))
  sns.violinplot(x = col, data = train, kde = True, ax = axs[0])
  sns.histplot(x = col, data = train, kde = True, ax = axs[1])

 

  - 히트맵으로 각 feature간 상관관계 파악

plt.figure(figsize = (15, 15))
sns.heatmap(train.corr(), cmap = 'vlag')

 

  - 히트맵으로 결측값 분포 확인(결측값이 있다면 그 부분이 살구색으로 표현됨)

plt.figure(figsize = (15, 15))
sns.heatmap(train.isnull())

 

 

5. 데이터 전처리(결측값 처리)

# 방법 1. 결측값이 있는 행 제거
feature = feature.dropna(axis = 0, how = 'any') # 각 행에서 하나의 값이라도 NaN이면 해당 행 제거
feature = feature.dropna(axis = 0, how = 'all') # 각 행의 모든 값이 NaN이면 해당 행 제거

# 방법 2. 결측값 채우기(특정 값으로 채우기)
feature = feature.fillna(0) # 0으로 채우기
feature = feature.fillna(method = 'pad')  # 바로 앞의 값으로 채우기
feature = feature.fillna(method = 'bfill')  # 바로 뒤의 값으로 채우기
feature = feature.fillna(feature.mean())  # 해당 열의 평균값으로 채우기

# 방법 3. 결측값 채우기(보간법)
feature = feature.interpolate(method = 'time') # 인덱스가 날짜/시간일 때 날짜나 시간에 따라 보간
feature = feature.interpolate(method = 'nearest', limit_direction = 'forward') # 가장 가까운 값으로 보간
feature = feature.interpolate(method = 'linear', limit_direction = 'forward') # 양 끝값에 따라 선형식에 적용하여 보간
feature = feature.interpolate(method = 'polynomial', order = 2) # 양 끝값에 따라 다항식에 적용하여 보간 (order은 다항식 차수)

# 방법 4. 결측값이 있는 열을 해당 열의 결측값이 아닌 행과 다른 feature 변수들로 예측하여 채우기
feature_target = feature.pop('결측값 열') # 다른 행들로부터 결측값을 예측할 목표열 분리
target_index = feature_target[feature_target['결측값 열'] == np.nan].index  # 목표열 중 결측값인 행의 인덱스 추출
feature_train = feature.iloc[~target_index] # 목표열이 아닌 다른 열에서 결측값이 있는 행의 인덱스가 아닌 행 추축(train 용)
feature_test = feature.iloc[target_index] # 목표열이 아닌 다른 열에서 결측값이 있는 행의 인덱스인 행 추출(test 용)
feature_target_train = feature_target[~target_index]  # 목표열 중 결측값이 아닌 행 추출(train 용)
rf = RandomForestRegressor()  # 랜덤포레스트로 예측
rf.fit(feature_train, feature_target_train) # 랜덤포레스트에 목표열 중 결측값이 아닌 행을 target 변수, 다른 열의 해당 행들을 feature 변수로 모델 피팅
pred = rf.predict(feature_test) # 피팅된 모델로 목표열에서 결측값인 행 예측
feature.iloc[target_index]['결측값 열'] = pred  # 원래 데이터프레임에서 목표열의 결측값인 값을 위에서 예측한 값으로 대체

 

 

6. 데이터 전처리(이상값 처리)

# IQR으로 이상값 판하여 절단
def outlier(df, col):
  tar = df[col]
  quantile_25 = np.percentile(tar.values, 25)
  quantile_75 = np.percentile(tar.values, 75)
  iqr = quantile_75 - quantile_25
  low = quantile_25 - iqr * 1.5
  high = quantile_75 + iqr * 1.5
  outlier_index = tar[(tar < low) | (tar > high)].index
  del_outlier = df.drop(outlier_index, axis = 0)
  return del_outlier

 

 

7. 데이터 전처리(표준화 및 정규화 = 피처 스케일링)

# 방법 1. StandardScaler
scaler = StandardScaler()
feature[[feature.select_dtypes([float, int]).columns]] = scaler.fit_transform(feature.select_dtypes([float, int]))
test[[test.select_dtypes([float, int]).columns]] = scaler.transform(test.select_dtypes([float, int]))

  -  평균 0, 분산 1

  - SVM, 선형회귀, 로지스틱 회귀는 데이터가 가우시안 정규 분포를 가지고 있다고 가정하고 구현되어 있기 때문에 StandardScaler가 예측 성능 향상에 도움

 

# 방법 2. MinMaxScaler
scaler = MinMaxScaler()
feature[[feature.select_dtypes([float, int]).columns]] = scaler.fit_transform(feature.select_dtypes([float, int]))
test[[test.select_dtypes([float, int]).columns]] = scaler.transform(test.select_dtypes([float, int]))

  - 최소값 0, 최대값 1

  - 음수값이 있다면 최소값 -1, 최대값 1

  - 데이터가 가우시안 정규 분포가 아닐 때 적용해볼 수 있음

 

 

8. 데이터 전처리(인코딩)

# 방법 1. 문자형 변수 라벨 인코딩
encoder = LabelEncoder()
feature[[feature.select_dtypes(object).columns]] = encoder.fit_transform(feature.select_dtypes(object))
test[[test.select_dtypes(object).columns]] = encoder.transform(test.select_dtypes(object))

 

# 방법 2. 문자형 변수 원핫 인코딩
encoder = OneHotEncoder()
feature[[feature.select_dtypes(object).columns]] = encoder.fit_transform(feature.select_dtypes(object).toarray())
test[[test.select_dtypes(object).columns]] = encoder.transform(test.select_dtypes(object).toarray())

  - 라벨 인코딩은 문자형 변수 고윳값의 개수에 따라 0부터 n-1까지의 숫자로 변환되어 ML에서 왜곡된 결과를 불러올 수 있음

  - 원핫 인코딩은 이런 문제를 해결하기 위해 고윳값의 개수만큼 새로운 feature 변수를 생성하여 각 행에서 고윳값에 해당하는 feature만 1, 나머지는 0으로 표시

 

 

9. 모델 피팅 및 예측

# feature 변수와 target 변수 분리
X = feature
y = target

# 학습용 데이터(X_train, y_train)와 검증용 데이터(X_test, y_test)로 분리
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = 0.2)

# 모델 선언(사용할 모델로 선정, 예시로는 RandomForestRegressor 선정)
model = RandomForestRegressor()

# 모델 학습
model.fit(X_train, y_train)

# 학습된 모델로 검증용 데이터 예측
y_pred = rf.predict(X_test)

# 실제 검증용 데이터와 예측한 검증용 데이터 비교하여 점수 출력
print(f'mse: {mean_squared_error(y_test, y_pred)}\nmae: {mean_absolute_error(y_test. y_pred)}\naccuracy: {accuracy_score(y_test, y_pred)}')

 

 

10. 교차 검증

# 방법 1. KFold
def exec_kfold(model, folds = 5):
  kfold = KFold(n_splits = folds)
  scores = []

  for iter, (train_index, test_index) in enumerate(kfold.split(X)):
    X_train, X_test = X.values[train_index], X.values[test_index]
    y_train, y_test = y.values[train_index], y.values[test_index]
    model.fit(X_train, y_train)
    y_pred = model.predict(X_test)
    accuracy = accuracy_score(y_test, y_pred)
    scores.append(accuracy)
    print(f'교차 검증 {iter} 정확도: {accuracy:.4f}')

  print(f'평균 정확도: {np.mean(scores):.4f}')

exec_kfold(model, folds = 5)
# 방법 2. cross_val_score
scores = cross_val_score(model, X, y, cv = 5)
for iter, accuracy in enumerate(scores):
  print(f'교차 검증 {iter} 정확도: {accuracy:.4f}')
print(f'평균 정확도: {np.mean(scores):.4f}')

  - KFold와 cross_val_score에서 각각 평균 정확도가 다른 이유는 cross_val_score가 StratifiedKFold를 이용해 폴드 세트를 분할하기 때문

 

# 방법 3. GridSearchCV
params = {'max_depth': [2, 3, 5, 10], 'min_samples_split': [2, 3, 5], 'min_samples_leaf': [1, 5, 8]}  # 사용할 모델에 맞는 하이퍼 파라미터 중 탐색하고 싶은 범위 지정
grid_model = GridSearchCV(model, param_grid = params, scoring = 'accuracy', cv = 5, verbose = 3)  # GridSearchCV에 model, 하이퍼 파라미터, 점수, 교차검증 횟수 등을 파라미터로 지정
grid_model.fit(X_train, y_train)  # GridSearchCV 모델에 train 데이터 피팅
print(f'GridSearchCV 최적 하이퍼 파라미터: {grid_model.best_params_}')  # 최적 하이퍼 파라미터
print(f'GridSearchCV 최고 정확도: {grid_model.best_score_:.4f}')  # 최적 하이퍼 파라미터일 때 나온 최고 점수
best_model = grid_model.best_estimator_ # 최적 하이퍼 파라미터로 최고 점수가 나온 모델을 best_model로 저장

y_pred = best_model.predict(X_test) # best_model로 test 데이터 예측
accuracy = accuracy_score(y_test, y_pred) # 점수 계산
print(f'테스트 세트에서의 model 정확도: {accuracy:.4f}')

 

 

11. 기본 분석 이후

  - pycaret, autogluon 등 auto ML을 사용해 최적의 모델 선택

  - 데이터에 대한 깊은 이해를 통해 더 최적의 변수 선택 및 전처리, 차원 축소 및 군집화 등으로 파생변수 생성

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. 프로젝트 결과물

    - 동작 방식

 

11. 행 삭제하기

  • 조건을 넣어 True인 값만 출력하도록 하면 조건에 맞지 않는 값은 삭제된 것과 같은 상태
import pandas as pd

df = pd.read_csv('titanic.csv')
# 데이터프레임에서 Sex열의 값이 male이 아닌 것 즉, female인 것만 출력, male은 삭제되고 female인 것만 출력됨
df[df['Sex'] != 'male'].head(2)

# 행의 인덱스를 사용해 하나의 행을 삭제할 수 있음
# 인덱스가 0인 행만 제외하고 나머지 출력
df[df.index != 0].head(2)

 

 

12. 중복된 행 삭제하기

  • drop_duplicates 사용(매개변수 사용에 주의)
# drop_duplicates를 매개변수 없이 사용하면 완벽하게 같은 행만 삭제되어 예시로 사용하는 타이타닉 데이터에서는 삭제되는 열이 없음
print('원본 데이터프레임 행의 수: {}'.format(len(df)))
print('drop_duplicates 사용한 데이터프레임 행의 수: {}'.format(len(df.drop_duplicates())))

# 출력 결과
원본 데이터프레임 행의 수: 891
drop_duplicates 사용한 데이터프레임 행의 수: 891
# 따라서 중복을 비교할 열을 subset 매개변수에 전달하여야 함
# Sex 변수에서 중복되는 값이 전부 제거되고 제일 처음 male값을 가지는 행과 female 값을 가지는 행만 남아 출력됨
df.drop_duplicates(subset = ['Sex'])

# keep 매개변수에 last를 전달하면 가장 마지막에 남은 male값과 female값만 하나씩 출력됨
df.drop_duplicates(subset = ['Sex'], keep = 'last')

 

 

13. 값에 따라 행을 그룹핑하기

  • groupby('그루핑할 열')['통계 계산을 할 열'].통계계산(mean, sum, count 등)
# 성별로 그루핑하여 각 성별의 나이 평균 계산
df.groupby('Sex')['Age'].mean()

# 출력 결과
Sex
female    27.915709
male      30.726645
Name: Age, dtype: float64

 

  • 첫 번째 열로 그루핑한 다음 두 번째 열로 다시 그룹핑하기
# 성별로 구루핑 하고 각 성별 중 생존 여부에 따라 다시 그루핑
# 이후 총 4개의 그룹 각각의 나이 평균 계산
df.groupby(['Sex', 'Survived'])['Age'].mean()

# 출력 결과
Sex     Survived
female  0           25.046875
        1           28.847716
male    0           31.618056
        1           27.276022
Name: Age, dtype: float64

 

 

14. 시간에 따라 행을 그루핑하기

  • 시간을 인덱스로 하는 예시 데이터프레임 생성
import pandas as pd
import numpy as np

# 2023/06/26부터 30초 간격으로 100,000개의 시간 데이터를 인덱스로 생성
time_index = pd.date_range('06/26/2023', periods = 100000, freq = '30s')

# 위에서 생성한 시간 인덱스를 인덱스로하고, 1~10사이의 숫자 중 랜덤하게 100,000개를 뽑은 것을 각 시간의 값으로 생성
df = pd.DataFrame(index = time_index)
df['Sale_Amount'] = np.random.randint(1, 10, 100000)

df

# 일주일 단위로 행을 그루핑
df.resample('W').sum()

  • 시간 단위
    • 'W': 1주 단위
    • '2W': 2주 단위
    • 'M': 1달 단위
    • 'MS': 1달 단위, 그루핑된 인덱스를 월의 시작 날짜로
  • label = 'left' 옵션: 인덱스를 그루핑된 그룹의 마지막 레이블이 아닌 그 전 그룹의 마지막 레이블로 변환
df.resample('M').sum()
df.resample('M', label = 'left').sum()
df.resample('MS').sum()

'M'
'M', label = 'left'
'MS'

 

 

15. 열 원소 순회하기

  • 데이터프레임의 열을 시퀀스처럼 다룰 수 있음
# 데이터프레임의 ['Name']열에 속한 값들을 하나씩 차례대로 name으로 지정하여 출력(상위 2개의 열에만 예시로 적용)
for name in df['Name'][0:2]:
    print(name.upper())

 

 

16. 모든 열 원소에 함수 적용하기

  • 데이터프레임의 특정 열의 모든 값에 동시에 함수를 적용
# 문자를 대문자로 출력하는 함수 생성
def uppercase(x):
    return x.upper()

# 위에서 생성한 함수를 데이터프레임의 'Name'열에 한번에 적용(상위 2개의 열에만 예시로 적용)
df['Name'].apply(uppercase)[0:2]

 

  • map 함수도 apply 함수와 비슷하지만 map 메서드는 딕셔너리를 입력으로 넣을 수 있고 apply 메서드는 매개변수를 지정할 수 있음
# 딕셔너리 형태로 Survived 열의 1값은 Live, 0값은 Dead로 변경
df['Survived'].map({1 : 'Live', 0 : 'Dead'})[:5]

 

  • apply 메서드에서 age라는 매개변수 전달
# age = 30이라는 매개변수 전달, Age열의 값이 전달받은 age보다 작은지에 대한 boolean 값 return
df['Age'].apply(lambda x, age: x < age, age = 30)[:5]

 

  • applymap 메서드로 전체 데이터프레임에 함수 적용 가능
# 문자값으로 이루어진 열에 대해 최대 길이를 20자로 제한하는 함수
def truncate_string(x):
    if type(x) == str:
        return x[:20]
    else:
        return x

전체 데이터프레임에 적용
df.applymap(truncate_string)[:5]

 

 

17. 그룹에 함수 적용하기

# Sex열로 그루핑하여 male 그룹과 female 그룹의 다른 열들 각각에 lambda 함수 적용
df.groupby('Sex').apply(lambda x: x.count())

 

 

18. 데이터프레임 연결하기

  • concat 함수
data_a = {'id': ['1', '2', '3'],
          'first': ['Alex', 'Amy', 'Allen'],
          'last': ['Anderson', 'Ackerman', 'Ali']}
df_a = pd.DataFrame(data_a, columns = ['id', 'first', 'last'])

data_b = {'id': ['4', '5', '6'],
          'first': ['Billy', 'Brian', 'Bran'],
          'last': ['Bonder', 'Black', 'Balwner']}
df_b = pd.DataFrame(data_b, columns = ['id', 'first', 'last'])

df_a
df_b

df_a
df_b

 

  • concat 함수에 axis = 0으로 하면 행 방향으로 데이터프레임 연결
pd.concat([df_a, df_b], axis = 0)

 

  • concat 함수에 axis = 1로 하면 열 방향으로 데이터프레임 연결
pd.concat([df_a, df_b], axis = 1)

 

 

19. 데이터프레임 병합하기

  • merge 함수
employee_data = {'employee_id': ['1', '2', '3', '4'],
                 'name': ['Amy Jones', 'Allen Keys', 'Alice Bees', 'Tim Horton']}
df_employees = pd.DataFrame(employee_data, columns = ['employee_id', 'name'])

sales_data = {'employee_id': ['3', '4', '5', '6'],
              'total_sales': [23456, 2512, 2345, 1455]}
df_sales = pd.DataFrame(sales_data, columns = ['employee_id', 'total_sales'])

df_employees
df_saeles

df_employees
df_sales

 

  • merge는 기본적으로 내부 조인을 실행(기준으로 하는 열(on = ''으로 지정한 열)이 같은 데이터만 병합하여 출력)
# 두 데이터프레임에서 employee_id가 두 곳 모두에 있는 값은 3, 4이므로 3, 4에 대해서만 다른 데이터가 병합되어 출력됨
pd.merge(df_employees, df_sales, on = 'employee_id')

 

  • how = '' 매개변수를 지정하여 outer, right, left 조인도 실행 가능
  • left, right 조인은 데이터프레임을 매개변수로 주는 순서에 따라 결과가 달라지므로 주의
pd.merge(df_employees, df_sales, on = 'employee_id', how = 'outer')

pd.merge(df_employees, df_sales, on = 'employee_id', how = 'left')

pd.merge(df_employees, df_sales, on = 'employee_id', how = 'right')

 

  • left와 right의 조인 기준열을 각각 지정할 수도 있음
pd.merge(df_employees, df_sales, left_on = 'employee_id', right_on = 'employee_id')

  • 데이터 랭글링은 원본 데이터를 정제하고 사용가능한 형태로 구성하기 위한 변환 과정을 광범위하게 의미하는 비공식 용어
  • 데이터 랭글링은 데이터 전처리의 한 과정
  • 데이터 랭글링에서 사용되는 가장 일반적인 데이터 구조는 데이터프레임

 

1. 데이터프레임 만들기

# 방법 1
import pandas as pd

df = pd.DataFrame()

df['Name'] = ['Jacky Jackson', 'Steven Stevenson']
df['Age'] = [38, 25]
df['Driver'] = [True, False]

df

# 방법 2(numpy 배열을 dataframe으로)
import numpy as np
data = [['Jacky Jackson', 38, True], ['Steven Stevencon', 25, False]]

matrix = np.array(data)
pd.DataFrame(matrix, columns = ['Name', 'Age', 'Driver'])

# or
pd.DataFrame(data, columns = ['Name', 'Age', 'Driver'])

# 방법 3(dictionary를 dataframe으로)
data = {'Name': ['Jacky Jackson', 'Steven Stevenson'],
        'Age': [38, 25],
        'Driver': [True, False]}
pd.DataFrame(data)

# 방법 4(각각의 데이터를 dictionary로, 각 dictionary를 병합)
data = [{'Name': 'Jacky Jackson', 'Age': 25, 'Driver': True},
        {'Name': 'Steven Stevenson', 'Age': 38, 'Driver': False}]
# 인덱스 따로 지정 가능
pd.DataFrame(data, index = ['row1', 'row2'])

 

 

2. 데이터 설명하기

  • 데이터를 적재한 후 확인하는 가장 간단한 방법은 head()를 통해 상위 몇 개의 데이터만 출력해서 확인해보는 것
  • tail()을 사용하면 하위 데이터 출력
df = pd.read_csv('titanic.csv')

# 데이터의 상위 2행만 출력
df.head(2)

 

  • 데이터의 열과 행의 개수 확인
df.shape

# 출력 결과
(891, 12)

 

  • describe()를 사용하여 숫자데이터 열의 통계값 확인
df.describe()

 

 

3. 데이터프레임 탐색하기

  • loc 또는 iloc를 사용해 하나 이상의 행이나 값을 선택
  • loc와 iloc 슬라이싱은 numpy와 달리 마지막 인덱스를 포함
# 첫번째 행 출력
df.iloc[0]

# 출력 결과
PassengerId                          1
Survived                             0
Pclass                               3
Name           Braund, Mr. Owen Harris
Sex                               male
Age                               22.0
SibSp                                1
Parch                                0
Ticket                       A/5 21171
Fare                              7.25
Cabin                              NaN
Embarked                             S
Name: 0, dtype: object
# 콜론(:)을 사용하여 행 슬라이싱
# 첫번째 행부터 네번째 행까지 출력
df.iloc[1:4]

# 슬라이싱의 첫 부분을 공백으로 두면 처음부터 선택
# 슬라이싱의 마지막 부분을 공백으로 두면 마지막까지 선택
df.iloc[:4]

# 데이터프레임에서 행마다 고유한 값을 갖는 특정 열을 인덱스로 설정
# loc를 사용해 고유값으로 해당 행을 슬라이싱 가능
df_name_index = df.set_index(df['Name'])
df_name_index.loc['Braund, Mr. Owen Harris']

# 출력 결과
PassengerId                          1
Survived                             0
Pclass                               3
Name           Braund, Mr. Owen Harris
Sex                               male
Age                               22.0
SibSp                                1
Parch                                0
Ticket                       A/5 21171
Fare                              7.25
Cabin                              NaN
Embarked                             S
Name: Braund, Mr. Owen Harris, dtype: object
# 열을 선택하여 선택한 열만 출력하기도 가능
df[['Age', 'Sex']].head(2)

 

 

4. 조건에 따라 행 선택하기

# 데이터프레임 df[] 안에 조건문인 df['Sex'] == female을 넣어서 True인 행만 출력
df[df['Sex'] == 'female'].head(2)

# &를 사용해 조건 두 개이상도 검색 가능
df[(df['Sex'] == 'female') & (df['Age'] >= 60)].head(2)

 

 

5. 값 치환하기

df['Sex'].replace('female', 'woman').head(2)

# 출력 결과
0     male
1    woman
Name: Sex, dtype: object


# 두 개 이상도 한번에 치환 가능
df['Sex'].replace(['female', 'male'], ['woman', 'man']).head(5)

# 출력 결과
0      man
1    woman
2    woman
3    woman
4      man
Name: Sex, dtype: object


# 한번에 여러 값은 하나의 값으로 치환 가능
df['Sex'].replace(['female', 'male'], 'person').head(5)

# 출력 결과
0    person
1    person
2    person
3    person
4    person
Name: Sex, dtype: object


# 치환할 여러 값을 딕셔너리 형태로 전달 가능
df['Sex'].replace({'female' : 1, 'male' : 0}).head(5)

# 출력 결과
0    0
1    1
2    1
3    1
4    0
Name: Sex, dtype: int64
# 위에서 하나의 열뿐 아니라 데이터프레임 전체의 모든 값을 한번에 치환 가능
df.replace(1, 'one').head(2)

 

 

6. 열 이름 바꾸기

df.rename(columns = {'Pclass' : 'Passenger Class'}).head(2)

 

  • 전체 열 이름을 바꿀 때 열이 많아 각각 입력하기 힘들다면 열 이름은 키로, 값은 비어있는 딕셔너리로 만들어 비어있는 값에 바꿀 열 이름을 넣어서 rename()에 전달하면 편리함
import collections

column_names = collections.defaultdict(str)

for name in df.columns:
    column_names[name]

column_names

# 출력 결과
defaultdict(str,
            {'PassengerId': '',
             'Survived': '',
             'Pclass': '',
             'Name': '',
             'Sex': '',
             'Age': '',
             'SibSp': '',
             'Parch': '',
             'Ticket': '',
             'Fare': '',
             'Cabin': '',
             'Embarked': ''})

 

  • rename(columns = {}) 대신 rename(index = {})으로 인덱스 치환가능
df.rename(index = {0:-1}).head(2)

 

  • 열 이름 소문자로 바꾸기
# 변환 함수 전달
df.rename(str.lower, axis = 'columns').head(2)

 

 

7. 최소값, 최대값, 합, 평균 계산 및 개수 세기

  • 각 통계값에 맞는 메서드 사용
    • 최대값: max()
    • 최소값: min()
    • 평균: mean()
    • 합: sum()
    • 개수: count()
    • 분산: var()
    • 표준편차: std()
    • 첨도: kurt()
    • 왜도: skew()
    • 평균의 표준오차: sem()
    • 최빈값: mode()
    • 중간값: median()
    • 상관계수: corr()
    • 공분산: cov()
print('최대값:', df['Age'].max())
print('최th값:', df['Age'].min())
print('평균:', df['Age'].mean())
print('합:', df['Age'].sum())
print('카운트:', df['Age'].count())

# 출력 결과
최대값: 80.0
최th값: 0.42
평균: 29.69911764705882
합: 21205.17
카운트: 714

 

  • 데이터프레임 전체에도 적용 가능
df.count()

# 출력 결과
PassengerId    891
Survived       891
Pclass         891
Name           891
Sex            891
Age            714
SibSp          891
Parch          891
Ticket         891
Fare           891
Cabin          204
Embarked       889
dtype: int64

 

 

8. 고유값 찾기

  • unique 메서드를 사용해 열에서 고유한 값을 모두 찾음
df['Sex'].unique()

# 출력 결과
array(['male', 'female'], dtype=object)

 

  • nunique 메서드는 unique 값의 개수를 출력
df['Sex'].nunique()

# 출력 결과
2


# nunique는 데이터프레임 전체에 적용 가능
df.nunique()

# 출력 결과
PassengerId    891
Survived         2
Pclass           3
Name           891
Sex              2
Age             88
SibSp            7
Parch            7
Ticket         681
Fare           248
Cabin          147
Embarked         3
dtype: int64

 

  • value_counts() 메서드를 사용해 열에서 고유한 값 각각의 개수 출력
df['Sex'].value_counts()

# 출력 결과
Sex
male      577
female    314
Name: count, dtype: int64

 

 

9. 누락된 값 다루기

  •  isnull()과 notnull() 메서드는 null인지 아닌지에 대한 답을 boolean값(True나 False)으로 나타냄
# 조건으로 isnull()을 주어 True나 False의 값이 나오므로 True인 행만 찾아 출력 가능
df[df['Age'].isnull()].head()

 

  • read_csv() 메서드에서 na_values = [] 옵션을 사용해 null 값으로 선언할 값들은 지정
  • read_csv() 메서드에서 keep_default_na 옵션을 False로 하여 pandas에서 기본적으로 null 값으로 인식하는 값을을 null값이 아니게 만들 수 있음
  • read_csv() 메서드에서 na_filter 옵션을 False로 하면 NaN 변환을 하지 않음

 

 

10. 열 삭제하기

  • drop() 메서드 사용
# 축은 열을 나타내는 1로 지정
df.drop('Age', axis = 1)

 

  • 한 번에 여러개의 열 삭제 가능
df.drop(['Age', 'Sex'], axis = 1)

 

  • 열 이름이 없다면 열의 인덱스 번호를 사용해 삭제 가능
# 인덱스가 1인 열(두번째 열) 삭제
df.drop(df.columns[1], axis = 1)

 

  • del df['Age']를 사용해 열을 삭제할 수 있지만 이는 원보 데이터프레임을 바꿔 원본 데이터의 손상을 유발할 수 있어 추천 X
  • inplace = True 옵션을 사용해도 원본 데이터가 변형되므로 사용 X
  • 또는, 원본 데이터는 놔두고 데이터를 조작할 새로운 데이터프레임을 복사하여 만들면 좋음
  • 머신러닝을 위한 원본 데이터는 로그 파일이나 데이터셋 파일, 데이터베이스, 여러 소스에서 추출
  • CSV, SQL 데이터베이스 등 다양한 소스에서 데이터를 적재하는 방법 학습
  • 실험에 필요한 특성을 가진 모의 데이터 생성 방법 학습
  • 외부 데이터 적재에는 판다스 / 모의 데이터 생성에는 사이킷런 사용

1. 샘플 데이터셋 적재하기

  • 사이킷런의 예제 데이터 사용
# pip install scikit-learn
from sklearn import datasets

# 숫자 데이터셋 적재
digits = datasets.load_digits()

# 특성 행렬
features = digits.data

# 타깃 벡터 생성
target = digits.target

# 첫번째 샘플 확인
features[0]

# 출력 결과
array([ 0.,  0.,  5., 13.,  9.,  1.,  0.,  0.,  0.,  0., 13., 15., 10.,
       15.,  5.,  0.,  0.,  3., 15.,  2.,  0., 11.,  8.,  0.,  0.,  4.,
       12.,  0.,  0.,  8.,  8.,  0.,  0.,  5.,  8.,  0.,  0.,  9.,  8.,
        0.,  0.,  4., 11.,  0.,  1., 12.,  7.,  0.,  0.,  2., 14.,  5.,
       10., 12.,  0.,  0.,  0.,  0.,  6., 13., 10.,  0.,  0.,  0.])

 

  • 사이킷런 예제 데이터들은 딕셔너리 구조를 가지고 있어 keays와 values를 반환함
digits.keys()

# 출력 결과
dict_keys(['data', 'target', 'frame', 'feature_names', 'target_names', 'images', 'DESCR'])


# DESCR 키는 데이터셋에 대한 설명
print(digits['DESCR'])

# 출력 결과
.. _digits_dataset:

Optical recognition of handwritten digits dataset
--------------------------------------------------

**Data Set Characteristics:**

    :Number of Instances: 1797
    :Number of Attributes: 64
    :Attribute Information: 8x8 image of integer pixels in the range 0..16.
    :Missing Attribute Values: None
    :Creator: E. Alpaydin (alpaydin '@' boun.edu.tr)
    :Date: July; 1998

This is a copy of the test set of the UCI ML hand-written digits datasets
https://archive.ics.uci.edu/ml/datasets/Optical+Recognition+of+Handwritten+Digits

The data set contains images of hand-written digits: 10 classes where
each class refers to a digit.

Preprocessing programs made available by NIST were used to extract
normalized bitmaps of handwritten digits from a preprinted form. From a
total of 43 people, 30 contributed to the training set and different 13
to the test set. 32x32 bitmaps are divided into nonoverlapping blocks of
4x4 and the number of on pixels are counted in each block. This generates
...
    2005.
  - Claudio Gentile. A New Approximate Maximal Margin Classification
    Algorithm. NIPS. 2000.

 

  • load_데이터셋의 유일한 매개변수인 return_X_y를 True로 하면 특성 X와 타깃 y배열을 반환
import numpy as np

# digits 함수는 특별히 n_class 매개변수를 사용하여 필요한 숫자 개수 지정가능
# 0~4까지 5개의 숫자만 y값에 넣도록 매개변수 설정
X, y = datasets.load_digits(n_class = 5, return_X_y = True)

np.unique(y)
# 출력 결과
array([0, 1, 2, 3, 4])

 

 

2. 모의 데이터셋 만들기

  • 선형 회귀 알고리즘에 사용하려면 make_regression 추천
  • make_regression은 실수 특성 행렬과 실수 타깃 벡터 반환
from sklearn.datasets import make_regression

# n_features는 전체 특성
# n_informative는 타깃 벡터 생성에 사용할 특성
features, target, coefiicients = make_regression(n_samples = 100,
                                                 n_features = 3,
                                                 n_informative = 3,
                                                 n_targets = 1,
                                                 noise = 0.0,
                                                 coef = True,
                                                 random_state = 1)
from sklearn.datasets import make_regression
features, target, coefiicients = make_regression(n_samples = 100,
                                                 n_features = 3,
                                                 n_informative = 3,
                                                 n_targets = 1,
                                                 noise = 0.0,
                                                 coef = True,
                                                 random_state = 1)
print('특성 행렬\n', features[:3])
print('타깃 벡터\n', target[:3])

# 출력 결과
특성 행렬
 [[ 1.29322588 -0.61736206 -0.11044703]
 [-2.793085    0.36633201  1.93752881]
 [ 0.80186103 -0.18656977  0.0465673 ]]
타깃 벡터
 [-10.37865986  25.5124503   19.67705609]

 

  • 분류 알고리즘에 사용하려면 make_classification 추천
  • make_classification은 실수 특성 행렬과 정수 타깃 벡터 반환
from sklearn.datasets import make_classification
# n_redundant는 필요없는 특성의 수, n_informative 특성의 랜덤 선형 결합으로 생성됨
# weights는 차례대로 각각 첫번째 ,두번째 클래스의 비율
features, target = make_classification(n_samples = 100,
                                                     n_features = 3,
                                                     n_informative = 3,
                                                     n_redundant = 0,
                                                     n_classes = 2,
                                                     weights = [.25, .75],
                                                     random_state = 1)
print('특성 행렬\n', features[:3])
print('타깃 벡터\n', target[:3])

# 출력 결과
특성 행렬
 [[ 1.06354768 -1.42632219  1.02163151]
 [ 0.23156977  1.49535261  0.33251578]
 [ 0.15972951  0.83533515 -0.40869554]]
타깃 벡터
 [1 0 0]

 

  • 군집 알고리즘에 사용하려면 make_blobs 추천
  • make_blobs은 실수 특성 행렬과 정수 타깃 벡터 반환
from sklearn.datasets import make_blobs
features, target = make_blobs(n_samples = 100,
                                       n_features = 2,
                                       centers = 3,
                                       cluster_std = 0.5,
                                       shuffle = True,
                                       random_state = 1)
print('특성 행렬\n', features[:3])
print('타깃 벡터\n', target[:3])

# 출력 결과
특성 행렬
 [[ -1.22685609   3.25572052]
 [ -9.57463218  -4.38310652]
 [-10.71976941  -4.20558148]]
타깃 벡터
 [0 1 1]
# 만들어진 군집 데이터 시각화
# pip install matplotlib
import matplotlib.pyplot as plt

plt.scatter(features[:, 0], features[:, 1], c = target)
plt.show()

 

 

3. CSV 파일 적재하기

  • pandas 라이브러리의 read_csv() 사용
  • 매개변수
    • sep = ',': 파일이 사용하는 구분자를 ','로 지정
    • skiprows = range(1, 11): 1행부터 12행까지 건너뛴 후 출력
    • nrows = 1: 한 행 출력
import pandas as pd

dataframe = pd.read_csv('csv 경로')

 

 

4. 엑셀 파일 적재하기

  • pandas 라이브러리의 read_excel() 사용
  • read_excel()을 사용하려면 xlrd 패키지 설치 필요
  • 매개변수
    • sheet_name: 시트 이름 문자열 또는 시트의 위치를 나타내는 정수(0부터 시작되는 인덱스)
    • na_filter: 결측값 탐지, 결측값이 없는 데이터라면 해당 옵션을 제외하는 것이 성능 향상에 도움
    • skip_rows
    • nrows
    • keep_default_na: 공백을 NA로 지정할지 결정
    • na_values = '-': 결측값을 '-'로 표시
# pip install xlrd
import pandas as pd

dataframe = pd.read_excel('excel 경로')

 

 

5. JSON 파일 적재하기

  • pandas 라이브러리의 read_json() 사용
  • 매개변수
    • orient = 'columns'(split, records, index, columns, values 중 하나): JSON 파일의 구성 형식 지정, 어떤 값을 넣어야하는 지 알아내기 위해서는 실험이 필요
      • 'split': {"index" : [인덱스, ...], "columns" : [열, ...], "data" : [값, ...]}
      • 'records': {[열 : 값}, ..., {열 : 값}]
      • 'index': {인덱스 : {열 : 값, ...}, ...}
      • 'values': [값, ...]
      • 'columns': {열: {인덱스 : 값, ...}, ...}
    • json_normalize: 구조화가 덜 된 JSON 데이터를 데이터프레임으로 변환하는 도구
import pandas as pd

dataframe = pd.read_json('json 경로', orient = 'columns')

 

 

6. SQL 데이터베이스로부터 적재하기

  • pandas 라이브러리의 read_sql_query() 사용
  • 먼저, SQLite 데이터베이스 엔진으로 연결하기 위해 create_engine 함수 사용
  • 이후, read_sql_query 함수로 SQL을 사용하여 데이터베이스에 질의, 그 결과를 데이터프레임으로 가져옴
# pip install sqlalchemy
import pandas as pd
from sqlalchemy import create_engine

# sample.db라는 데이터베이스에 연결
database_connection = create_engine('sqlite:///sample.db')

# sample.db에서 data라는 이름의 테이블의 모든 열을 반환하라고 요청
dataframe = pd.read_sql_query('SELECT * FROM data', database_connection)

# 모든 행을 가져올 때는 질의문 없이 read_sql_table() 함수도 사용 가능
dataframe = pd.read_sql_table('data', database_connection)

+ Recent posts