cs
written in 2022, Jan 26
우리가 마주하는 대부분의 CSV 파일에서 수치 데이터는 각 숫자가 나열된 상태로 되어있지만, 일부 수치 데이터는 매 천의 자리마다 콤마가 적힌 포맷으로 간주하여 객체 자료형, 즉 문자열로 읽어드리는 경우가 있다. 그러한 경우, .replace()
함수를 통해 콤마를 제거한 후 수치 자료형으로 변경하면 간단하게 해결할 수 있다.
위와 같은 경우는 보통 우리가 다른 부서와 협업하며 얻어지는 데이터보다는 웹 크롤링에 의해 얻어진 데이터를 다루는 경우에 자주 발생하게 된다. 같은 상황을 가정하기 위해서 우리는 직접 웹 크롤링하여 분석하는 데이터 전처리하는 상황을 가정하고 체험해보도록 하자.
소스코드는 『파이썬 데이터 클리닝 쿡북』(마이클워커 지음)에서 발췌해서 일부 가공하였다.
데이터 수집을 위해서 CloudTables 홈페이지에 있는 직원별 정보 테이블 예제를 requests
와 bs4
패키지를 통해 웹 크롤링하여 가져온다. HTML 테이블 구조로 짜여있는 pd.DataFrame
에 맞게 데이터 구조를 재구축한다. 그 후 $
, ,
와 같은 특수문자를 제거하는 데이터 전처리 과정 이후, 칼럼 별 알맞은 데이터 타입 선언하여 마무리한다.
pd.DataFrame
포맷으로 변경pd.DataFrame.values
에서 특수문자 제거하기
HTML(DOM) sourced data
: 직원별 이름, 직무, 지사, 나이, 근무시작일, 월급을 포함하는 테이블
웹 크롤링을 하기위해 Python
에서는 용도에 맞는 여러 패키지들을 지원하지만, 그중 우리는 프로토콜을 통해 페이지에 데이터를 요청하는 requests
패키지와 요청한 데이터를 고정(parsing)할 수 있도록 하는 bs4
패키지를 사용한다. 각 패키지들을 사용하기 이전에 import
명령어를 통해 컴퓨터 메모리 상에 올려서 분석을 준비한다.
>>> import pandas as pd
>>> import numpy as np
>>> import requests
>>> from bs4 import BeautifulSoup
간단하게 requests.get()
명령어를 통해 해당 주소를 로드한 후엔, 프로토콜이 제대로 응답이 되었다는 <Response [200]>
메세지를 확인한다면, BeautifulSoup
객체로 만들어 고정한다.
>>> webpage = requests.get("https://datatables.net/examples/data_sources/dom")
>>> webpage
<Response [200]>
HTML 구조가 BeautifulSoup
객체로 고정된 이후엔 bs.find()
함수를 통해서 구체적인 HTML 태그명과 id, class 등을 통해 우리가 원하는 부분만 추출할 수 있다. HTML 테이블의 경우 태그명이 table
이며, 대부분 th
와 td
태그에 헤더와 테이블 값이 존재함을 기억하자.
이 예제에서 크롤링을 위해 크롬의 개발자 도구를 통해 HTML 구조를 확인하였을 때, 테이블 헤더와 값은 각각 th
와 td
태그 안에 있었다.
일단 해당 페이지의 HTML 구조가 파악되면, 태그명과 id 값을 입력하여 데이터를 호출할 수 있는지 확인한다.
>>> theadrows = bs.find('table', {'id':'example'}).thead.find_all('th')
>>> theadrows
[<th>Name</th>,
<th>Position</th>,
<th>Office</th>,
<th>Age</th>,
<th>Start date</th>,
<th>Salary</th>]
같은 방식으로 데이터 값을 불러온다.
>>> rows = bs.find('table', {'id':'example'}).tbody.find_all('td')
>>> rows[:6]
[<td>Tiger Nixon</td>,
<td>System Architect</td>,
<td>Edinburgh</td>,
<td>61</td>,
<td>2011/04/25</td>,
<td>$320,800</td>]
pd.DataFrame
의 구조는 파이썬 딕셔너리 형태와 비슷하다. 각 칼럼명과 데이터가 key
와 value
의 관계로 이루어져 있다. 반면에 pd.DataFrame
객체를 만들기 위해 선언하는 방식은 여러 가지가 있는데, 여기선 칼럼명과 데이터 값을 리스트 파일로 만들어 넘기는 방식을 사용한다.
앞서서 bs4.element.ResultSet
값이 제대로 불러와졌다면 .get_text()
메서드를 통해 태그를 제거하고 문자열만 추출하도록 한다.
>>> labelcols = [j.get_text() for j in theadrows]
>>> labelcols
['Name', 'Position', 'Office', 'Age', 'Start date', 'Salary']
마찬가지로 데이터 값이 담긴 datarows
에서도 태그를 제거하고 의미 있는 문자열만 추출한다. 단 여기서는 인덱스 별로 데이터를 구분하기 위해서 np.ndarray
로 변환하고 .reshape()
함수를 통해 각각 6개의 레이블을 가지도록 재구조화한다. .reshape(M, N)
에서 -1
은 항상 row
나 col
기준으로 나눈 후 나머지는 자동으로 계산되어 들어가는 것을 의미함을 기억하자. 여기선 col=6
을 기준으로 데이터를 나눈 후, row
개수가 자동으로 채택됨을 의미한다.
>>> datarows = np.array([row.get_text() for row in rows]).reshape(-1, 6)
>>> datarows[0]
array(['Tiger Nixon', 'System Architect', 'Edinburgh', '61', '2011/04/25',
'$320,800'], dtype='<U29')
마지막으로 labelcols
와 datarows
를 positional arguement
에 맞게 입력해주면 pd.DataFrame
이 완성된다.
>>> salary = pd.DataFrame(datarows,
>>> columns=labelcols).sort_values('Start date').reset_index(drop=True)
>>> salary.head()
우선 칼럼 별로 dtpye
을 지정하기에 앞서서, 칼럼명을 자주 소환해야 하기 때문에 모두 소문자로 만들고, 공백을 _
로 채워 snake_case(a.k.a. underscore case)로 만들어준다. (칼럼에 공백란이 존재하는 경우, 프로그래밍상 오류가 생길 수 있기 때문)
salary.columns = salary.columns.str.replace(" ", "_").str.lower()
데이터 타입을 지정하기 이전에 각 칼럼은 .read_csv()
로 불러들인 다른 데이터프레임과 다르게 모두 object
형으로 초기화되어 있다.
>>> salary.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 57 entries, 0 to 56
Data columns (total 6 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 name 57 non-null object
1 position 57 non-null object
2 office 57 non-null object
3 age 57 non-null object
4 start_date 57 non-null object
5 salary 57 non-null object
dtypes: object(6)
memory usage: 2.8+ KB
문제가 되는 데이터 칼럼에서 값을 추출하여 확인하고, 체이닝 룰을 통해 어떤 방식으로 해결해야 하는지 확인한다.
>>> salary.iloc[0, -1]
'$645,750'
>>> salary.iloc[0, -1].replace(",", "").replace("$", "")
'645750'
한 케이스에 대해 문제 해결법을 찾았다면, pd.Series
에 대해서 가능하도록 확장해서 적용한다.
여기서 중요한 점은 .str
명령어를 사용하면 pd.Series
데이터에 대해서 pd.StringMethods
로 변환하여 문자열 자료형에 대해 일부만 수정하는 여러 메서드를 사용할 수 있다는 점이다. 해당 예제에서는 .replace(pat, repl, regex=True)
함수와 regex 문법을 사용하여 숫자를 제외한 모든 문자(특수문자 등)를 제거하였다.
데이터 정제가 끝난 이후엔 .dtype
을 수동으로 지정해줘서 이후에 분석을 용이하게 한다.
>>> salary['salary'] = salary['salary'].str.replace("[^0-9]", "", regex=True).astype('int64')
>>> salary['salary'].dtype
dtype('int64')
salary['salary']
시리즈에 대한 데이터 클리닝과 타입 선언이 끝난 이후엔, 다른 칼럼들 역시 분석하기 용이하도록 자료형에 맞는 데이터 타입을 지정해준다.
>>> for col in salary.columns[1:3]:
>>> salary[col] = salary[col].astype('category')
>>>
>>> salary['age'] = salary['age'].astype('int')
>>> salary['start_date'] = salary['start_date'].astype('datetime64')
>>>
>>> salary.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 57 entries, 0 to 56
Data columns (total 6 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 name 57 non-null object
1 position 57 non-null category
2 office 57 non-null category
3 age 57 non-null int32
4 start_date 57 non-null datetime64[ns]
5 salary 57 non-null int64
dtypes: category(2), datetime64[ns](1), int32(1), int64(1), object(1)
memory usage: 3.4+ KB
간단하게 .describe()
함수를 써서 salary['salary']
시리즈에 대한 기초 통계량을 확인할 수 있다.
>>> salary['salary'].describe()
count 57.00
mean 252,135.26
std 215,384.68
min 75,650.00
25% 112,000.00
50% 164,500.00
75% 327,900.00
max 1,200,000.00
Name: salary, dtype: float64
p.s. 자료를 찾다 보니 .read_html()
함수를 통해 HTML 테이블에 대해 더 빠르고 간편하게 크롤링을 하는 예제가 있어서 아래 첨부한다.
pd.Series
데이터에 대해 .str
명령어를 통해 pd.StringMethods
객체로 만든 후 .replace()
함수와 RegEX 표현을 통해 숫자를 제외한 나머지 특수문자를 없앨 수 있다..replace()
체이닝을 통해 제거할 수 있다..astype('int')
형변환을 통해 칼럼 데이터 타입을 새로 선언한다.[KOR] [Pandas] PeriodIndex vs. DatetimeIndex (0) | 2022.01.02 |
---|
댓글 영역