cs

상세 컨텐츠

본문 제목

[KOR] [Pandas] Removing Commas from Values in Pandas

2 Main Languages/Python

by shin15530 2022. 1. 26. 20:09

본문

written in 2022, Jan 26

우리가 마주하는 대부분의 CSV 파일에서 수치 데이터는 각 숫자가 나열된 상태로 되어있지만, 일부 수치 데이터는 매 천의 자리마다 콤마가 적힌 포맷으로 간주하여 객체 자료형, 즉 문자열로 읽어드리는 경우가 있다. 그러한 경우, .replace()함수를 통해 콤마를 제거한 후 수치 자료형으로 변경하면 간단하게 해결할 수 있다.

위와 같은 경우는 보통 우리가 다른 부서와 협업하며 얻어지는 데이터보다는 웹 크롤링에 의해 얻어진 데이터를 다루는 경우에 자주 발생하게 된다. 같은 상황을 가정하기 위해서 우리는 직접 웹 크롤링하여 분석하는 데이터 전처리하는 상황을 가정하고 체험해보도록 하자.

소스코드는 『파이썬 데이터 클리닝 쿡북』(마이클워커 지음)에서 발췌해서 일부 가공하였다.

 

Overview

데이터 수집을 위해서 CloudTables 홈페이지에 있는 직원별 정보 테이블 예제를 requestsbs4 패키지를 통해 웹 크롤링하여 가져온다. HTML 테이블 구조로 짜여있는 pd.DataFrame에 맞게 데이터 구조를 재구축한다. 그 후 $, ,와 같은 특수문자를 제거하는 데이터 전처리 과정 이후, 칼럼 별 알맞은 데이터 타입 선언하여 마무리한다.

  1. 웹 크롤링: CloudTables 사이트에서 HTML table을 스크래핑
  2. 데이터 재구조화: 크롤링 데이터를 pd.DataFrame 포맷으로 변경
  3. 데이터 전처리: pd.DataFrame.values에서 특수문자 제거하기

 

Procedure

0. Data Description

  • HTML(DOM) sourced data: 직원별 이름, 직무, 지사, 나이, 근무시작일, 월급을 포함하는 테이블

 

1. Web Crawling

웹 크롤링을 하기위해 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이며, 대부분 thtd 태그에 헤더와 테이블 값이 존재함을 기억하자.

이 예제에서 크롤링을 위해 크롬의 개발자 도구를 통해 HTML 구조를 확인하였을 때, 테이블 헤더와 값은 각각 thtd 태그 안에 있었다.

Figure 1. screenshot of HTML Table Header Tag
Figure 2. screenshot of HTML Table Content Tag

 

일단 해당 페이지의 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>]

 

2. Data Restructure

pd.DataFrame의 구조는 파이썬 딕셔너리 형태와 비슷하다. 각 칼럼명과 데이터가 keyvalue의 관계로 이루어져 있다. 반면에 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은 항상 rowcol 기준으로 나눈 후 나머지는 자동으로 계산되어 들어가는 것을 의미함을 기억하자. 여기선 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')

 

마지막으로 labelcolsdatarowspositional arguement에 맞게 입력해주면 pd.DataFrame이 완성된다.

>>> salary = pd.DataFrame(datarows,
>>>                       columns=labelcols).sort_values('Start date').reset_index(drop=True)
>>> salary.head()

Figure 3. screenshot of salary dataframe

 

3. Data Processing

우선 칼럼 별로 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 테이블에 대해 더 빠르고 간편하게 크롤링을 하는 예제가 있어서 아래 첨부한다.

 

Summary

  • pd.Series 데이터에 대해 .str 명령어를 통해 pd.StringMethods 객체로 만든 후 .replace() 함수와 RegEX 표현을 통해 숫자를 제외한 나머지 특수문자를 없앨 수 있다.
  • 단일 데이터에 대해서 지우고 싶다면, .replace() 체이닝을 통해 제거할 수 있다.
  • 특수문자가 모두 제거되었다면, .astype('int') 형변환을 통해 칼럼 데이터 타입을 새로 선언한다.

References

  • Michael Walker. (2020). Python Data Cleaning Cookbook. link
  • Pandas Documentation. (2022). pandas.Series.str.replace. link

Data Citations

  • CloudTables. (2022). HTML(DOM) sourced data. Web

Additional Readings

  • Chris Moffitt. (2020). Practical Business Python. link

'2 Main Languages > Python' 카테고리의 다른 글

[KOR] [Pandas] PeriodIndex vs. DatetimeIndex  (0) 2022.01.02

관련글 더보기

댓글 영역