티스토리 뷰
4장 - 셀프 주유소는 정말 저렴할까¶
이번 장에서는 아주 간단한 질문에 답하는 것을 함꼐 해보려 한다. 누군가 셀프 주유소는 정말 저렴한가? 에 대해 물었다면 데이터를 만지는 사람들은 어떻게 할까? 답은 바로 주유소의 가격을 조사해서 셀프 주유소와 아닌 주유소를 구분해서 비교하면 된다. 앞서 우리는 BeautifulSoup를 사용했다. 그걸로도 많은 일을 할 수 있다. 하지만 몇가지 문제로 인해 BeautifulSoup만으로는 접근할 수 없는 인터넷 정보가 있다. 그래서 사용하는 것이 바로 Selenium 이다. 우선 주유소의 가격을 비교하는 정부 사이트인 Opninet에 접속해서 정보를 모아야 한다.
from selenium import webdriver
먼저 selenium 에서 webdriver를 import 한다. 그리고 네이버(www.naver.com)에 접속해 보자.
driver = webdriver.Chrome('C:/Users/이재윤/Downloads/chromedriver_win32/chromedriver')
driver.get("http://naver.com")
위처럼 네이버에 접속을 하면 'chrome이 자동화된 테스트 소프트웨어에 의해 제어되고 있습니다' 라는 문구와 함께 웹 브라우저가 또 하나 떠 있을 텐데, 저 웹 브라우저는 우리가 코드로 움직일 브라우저이다. 그래서 가급적 우리가 생성한 크롬 브라우저는 손으로 조작하면 안된다. 코드를 작성할 때 혼선이 생길 수 있다. 별도로 네이버창을 하나 더 열어서 작업하자.
driver.save_screenshot('C:/Users/이재윤/images/001.jpg')
True
selenium은 save_screenshot 명령으로 화면을 캡처할 수 있다.
elem_login = driver.find_element_by_id("id")
elem_login.clear()
elem_login.send_keys("wodbsdbsk")
elem_login = driver.find_element_by_id("pw")
elem_login.clear()
elem_login.send_keys("sorkwodbs1!")
네이버에 로그인 정보를 입력하는 곳이 있다. 크롬 드라이버로 네이버에 로그인하고 싶다면 당연히 id와 비밀번호를 입력해야 한다. 개발자 도구를 이용해서 id와 비밀번호를 입력하는 부분의 html 소스 코드를 확인해 보면 id= 라는 항목에 id 혹은 pw 라고 되어있다. selenium이 제공하는 명령 중 find_element_by_id 를 이용해서 id와 pw를 찾으면 된다. 위 코드를 치면 로그인 정보가 입력되어 있을 것이다.
그런 후에 로그인 개발자 도구로 로그인 버튼 쪽을 클릭하면 특정 코드가 하이라이트 되어 있을 것이다. 그곳에서 마우스 오른쪽 버튼을 클릭 후 copy 항목으로 가서 copy xpath를 선택하면 로그인 버튼의 xpath를 복사할 수 있다.
xpath = '''//*[@id="frmNIDLogin"]/fieldset/input'''
driver.find_element_by_xpath(xpath).click()
그리고 위 코드를 작성하면서 방금 복사한 xpath를 xpath = 뒤에 붙여 넣기 하면 된다. 이렇게 로그인을 한 후 메일(mail.naver.com)에 접근해 보자.
driver.get("http://mail.naver.com")
원하는 곳으로 이동 했다면 Beautiful Soup를 이용해서 페이지 내용을 읽어오게 된다.
from bs4 import BeautifulSoup
html = driver.page_source
soup = BeautifulSoup(html, 'html.parser')
driver.page_source를 사용하면 현재 selenium이 접근한 페이지의 소스를 넘겨 받을 수 있다.
이제 크롬 개발자 도구를 이용해서 메일을 보낸 사람이 나타나는 곳의 태그를 확인해두자.
raw_list = soup.find_all('div', 'name _ccr(lst.from)')
raw_list
[<div class="name _ccr(lst.from)"><span class="blind">보낸 이:</span><a class="_c1(myContextMenu|showSenderContextLayer|list|4) _stopDefault" href="#" title='"네이버메일" <navermail_noreply@navercorp.com>'>네이버메일</a></div>, <div class="name _ccr(lst.from)"><span class="blind">보낸 이:</span><a class="_c1(myContextMenu|showSenderContextLayer|list|3) _stopDefault" href="#" title="<no_reply@its-lil.com>">no_reply@its-lil.com</a></div>, <div class="name _ccr(lst.from)"><span class="blind">보낸 이:</span><a class="_c1(myContextMenu|showSenderContextLayer|list|2) _stopDefault" href="#" title='"네이버" <webmaster@naver.com>'>네이버</a></div>, <div class="name _ccr(lst.from)"><span class="blind">보낸 이:</span><a class="_c1(myContextMenu|showSenderContextLayer|list|1) _stopDefault" href="#" title='"네이버" <webmaster@naver.com>'>네이버</a></div>]
위처럼 find_all 명령을 사용하면 된다.
send_list = [raw_list[n].find('a').get_text() for n in range(0, len(raw_list))]
send_list
['네이버메일', 'no_reply@its-lil.com', '네이버', '네이버']
손쉽게 메일을 보낸 사람의 리스트를 확보할 수 있다. 이제 크롬 드라이버를 닫아야 한다.
driver.close()
close()명령을 이용해서 실행된 크롬 드라이버를 종료할 수 있다.
4-2 서울시 구별 주유소 가격 정보 얻기¶
앞에서 배운 selenium 지식으로 https://goo.gl/VH1A5t 에 접속해서 서울시 구별 주유소 정보를 받아오자.
from selenium import webdriver
driver = webdriver.Chrome('C:/Users/이재윤/Downloads/chromedriver_win32/chromedriver')
driver.get('http://www.naver.com')
driver.get('http://www.opinet.co.kr/searRgSelect.do')
우리는 서울시만 검색할 것이니 가만히 두고, 처음에 나타나는 구에 종로구 라는 글자가 있는 부분은 바꿔줘야 한다. 리스트 박스 형태로 되어 있어서 해당 리스트의 내용을 받아와서 순차적으로 반환해주면 된다.
gu_list_raw = driver.find_element_by_xpath('''//*[@id="SIGUNGU_NM0"]''')
gu_list = gu_list_raw.find_elements_by_tag_name('option')
크롬 개발자 도구를 이용해서롬 개발자 도구를 이용해서 구 이름이 위치하는 곳을 클릭하면 나타나는 우측 코드에 마우스 오른쪽 버튼으로 Xpath를 복사하기를 하면 된다. 그렇게 확보한 xpath를 이용해서 element를 찾고 gu_list_raw 변수에 저장한다. 구 리스트는 find_elements_by_tag_name으로 option이라는 태그를 찾으면 된다.
gu_names = [option.get_attribute("value") for option in gu_list]
gu_names.remove('')
gu_names
['강남구', '강동구', '강북구', '강서구', '관악구', '광진구', '구로구', '금천구', '노원구', '도봉구', '동대문구', '동작구', '마포구', '서대문구', '서초구', '성동구', '성북구', '송파구', '양천구', '영등포구', '용산구', '은평구', '종로구', '중구', '중랑구']
그렇게 얻은 결과가 나타난다. 구 이름을 전체적으로 다 알았다. 구 이름이 있는 태그에 위 코드에서 저장한 gu_names에서 첫 번째 것을 한번 시험 삼아 입력하자.
element = driver.find_element_by_id("SIGUNGU_NM0")
element.send_keys(gu_names[0])
driver.save_screenshot('C:/Users/이재윤/images/002.jpg')
True
그러면 구 이름부분이 변경된 것을 알 수 있다. 그리고 조회 버튼을 누르면 된다. 조회 버튼의 xpath도 알아낼 수 있다.
xpath ='''//*[@id="searRgSelect"]/span'''
element_sel_gu = driver.find_element_by_xpath(xpath).click()
그리고 해당 xpath를 찾아서 click()을 붙여 주면 된다. 결과가 나타난 곳 아래에 엑셀저장 버튼을 눌러서 엑셀로 내용을 저장해야 한다. 지금까지 한 것처럼 xpat를 알아내서 엑셀 저장 버튼을 누른다.
xpath = """//*[@id="glopopd_excel"]/span"""
element_get_excel = driver.find_element_by_xpath(xpath).click()
이렇게 엑셀로 저장 버튼까지 누르고 나면 당연히 크럼 드라이버가 실행하는 브라우저가 지정된 다운로드 폴더에 파일을 다운로드 한다.
import time
from tqdm import tqdm_notebook
for gu in tqdm_notebook(gu_names):
element = driver.find_element_by_id('SIGUNGU_NM0')
element.send_keys(gu)
time.sleep(2)
xpath ='''//*[@id="searRgSelect"]/span'''
element_sel_gu = driver.find_element_by_xpath(xpath).click()
time.sleep(1)
xpath = '''//*[@id="glopopd_excel"]/span'''
element_get_excel = driver.find_element_by_xpath(xpath).click()
time.sleep(1)
<ipython-input-79-da81b98703dc>:4: TqdmDeprecationWarning: This function will be removed in tqdm==5.0.0 Please use `tqdm.notebook.tqdm` instead of `tqdm.tqdm_notebook` for gu in tqdm_notebook(gu_names):
이제 서울시 25개 구에 대해 반복문을 수행하면 된다.적절히 중간중간에 기다리라는 time.sleep 명령을 사용했고 25개 구에 대한 주유소 기름값을 저장한 엑셀파일이 다운로드 폴더에 저장된다.
driver.close()
4-3 구별 주유 가격에 대한 데이터의 정리¶
앞서 받은 25개의 엑셀 파일을 우리가 다루는 data 폴더로 옮긴다. 이전에 배운 엑셀 파일을 read하는 명령으로 하나하나 읽으면 25줄을 입력해야 하지만 파이썬에는 이를 해결해줄 좋은 모듈이 있다. pandas와 파일 glob 라고 하는 파일 경로등을 쉽게 접근할 수 있게 해주는 모듈을 import 하자.
import pandas as pd
from glob import glob
glob('C:/Users/이재윤/DataScience/data/지역*xls')
['C:/Users/이재윤/DataScience/data\\지역_위치별(주유소) (1).xls', 'C:/Users/이재윤/DataScience/data\\지역_위치별(주유소) (10).xls', 'C:/Users/이재윤/DataScience/data\\지역_위치별(주유소) (11).xls', 'C:/Users/이재윤/DataScience/data\\지역_위치별(주유소) (12).xls', 'C:/Users/이재윤/DataScience/data\\지역_위치별(주유소) (13).xls', 'C:/Users/이재윤/DataScience/data\\지역_위치별(주유소) (14).xls', 'C:/Users/이재윤/DataScience/data\\지역_위치별(주유소) (15).xls', 'C:/Users/이재윤/DataScience/data\\지역_위치별(주유소) (16).xls', 'C:/Users/이재윤/DataScience/data\\지역_위치별(주유소) (17).xls', 'C:/Users/이재윤/DataScience/data\\지역_위치별(주유소) (18).xls', 'C:/Users/이재윤/DataScience/data\\지역_위치별(주유소) (19).xls', 'C:/Users/이재윤/DataScience/data\\지역_위치별(주유소) (2).xls', 'C:/Users/이재윤/DataScience/data\\지역_위치별(주유소) (20).xls', 'C:/Users/이재윤/DataScience/data\\지역_위치별(주유소) (21).xls', 'C:/Users/이재윤/DataScience/data\\지역_위치별(주유소) (22).xls', 'C:/Users/이재윤/DataScience/data\\지역_위치별(주유소) (23).xls', 'C:/Users/이재윤/DataScience/data\\지역_위치별(주유소) (24).xls', 'C:/Users/이재윤/DataScience/data\\지역_위치별(주유소) (3).xls', 'C:/Users/이재윤/DataScience/data\\지역_위치별(주유소) (4).xls', 'C:/Users/이재윤/DataScience/data\\지역_위치별(주유소) (5).xls', 'C:/Users/이재윤/DataScience/data\\지역_위치별(주유소) (6).xls', 'C:/Users/이재윤/DataScience/data\\지역_위치별(주유소) (7).xls', 'C:/Users/이재윤/DataScience/data\\지역_위치별(주유소) (8).xls', 'C:/Users/이재윤/DataScience/data\\지역_위치별(주유소) (9).xls', 'C:/Users/이재윤/DataScience/data\\지역_위치별(주유소).xls']
위와 같이 ./data 폴더 안에 지역으로 시작하는 xls 파일 전체를 의미하는 ../data/지역*.xls과 같은 명령을 사용할 수 있다.
stations_files = glob('C:/Users/이재윤/DataScience/data/지역*xls')
stations_files
['C:/Users/이재윤/DataScience/data\\지역_위치별(주유소) (1).xls', 'C:/Users/이재윤/DataScience/data\\지역_위치별(주유소) (10).xls', 'C:/Users/이재윤/DataScience/data\\지역_위치별(주유소) (11).xls', 'C:/Users/이재윤/DataScience/data\\지역_위치별(주유소) (12).xls', 'C:/Users/이재윤/DataScience/data\\지역_위치별(주유소) (13).xls', 'C:/Users/이재윤/DataScience/data\\지역_위치별(주유소) (14).xls', 'C:/Users/이재윤/DataScience/data\\지역_위치별(주유소) (15).xls', 'C:/Users/이재윤/DataScience/data\\지역_위치별(주유소) (16).xls', 'C:/Users/이재윤/DataScience/data\\지역_위치별(주유소) (17).xls', 'C:/Users/이재윤/DataScience/data\\지역_위치별(주유소) (18).xls', 'C:/Users/이재윤/DataScience/data\\지역_위치별(주유소) (19).xls', 'C:/Users/이재윤/DataScience/data\\지역_위치별(주유소) (2).xls', 'C:/Users/이재윤/DataScience/data\\지역_위치별(주유소) (20).xls', 'C:/Users/이재윤/DataScience/data\\지역_위치별(주유소) (21).xls', 'C:/Users/이재윤/DataScience/data\\지역_위치별(주유소) (22).xls', 'C:/Users/이재윤/DataScience/data\\지역_위치별(주유소) (23).xls', 'C:/Users/이재윤/DataScience/data\\지역_위치별(주유소) (24).xls', 'C:/Users/이재윤/DataScience/data\\지역_위치별(주유소) (3).xls', 'C:/Users/이재윤/DataScience/data\\지역_위치별(주유소) (4).xls', 'C:/Users/이재윤/DataScience/data\\지역_위치별(주유소) (5).xls', 'C:/Users/이재윤/DataScience/data\\지역_위치별(주유소) (6).xls', 'C:/Users/이재윤/DataScience/data\\지역_위치별(주유소) (7).xls', 'C:/Users/이재윤/DataScience/data\\지역_위치별(주유소) (8).xls', 'C:/Users/이재윤/DataScience/data\\지역_위치별(주유소) (9).xls', 'C:/Users/이재윤/DataScience/data\\지역_위치별(주유소).xls']
이제 station_files 변수에 각 엑셀 파일의 경로와 이름을 리스트로 저장한다.
tmp_raw = []
for file_name in stations_files:
tmp = pd.read_excel(file_name, header=2)
tmp_raw.append(tmp)
station_raw = pd.concat(tmp_raw)
그리고 read_excel로 각 파일을 반복문을 이용해서 읽은 후 tml_raw 변수에 append 시킨다. 반복문이 끝나고 나면 concat 명령으로 쉽게 하나로 합칠 수 있다.
station_raw.info()
<class 'pandas.core.frame.DataFrame'> Int64Index: 537 entries, 0 to 45 Data columns (total 10 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 지역 537 non-null object 1 상호 537 non-null object 2 주소 537 non-null object 3 상표 537 non-null object 4 전화번호 537 non-null object 5 셀프여부 537 non-null object 6 고급휘발유 537 non-null object 7 휘발유 537 non-null object 8 경유 537 non-null object 9 실내등유 537 non-null object dtypes: object(10) memory usage: 46.1+ KB
총 545개의 주유소 정보가 저장된 것을 알 수 있다. 하지만 가격 정보가 숫자형(int, float)이 아니어서 나중에 처리하자.
station_raw.head()
지역 | 상호 | 주소 | 상표 | 전화번호 | 셀프여부 | 고급휘발유 | 휘발유 | 경유 | 실내등유 | |
---|---|---|---|---|---|---|---|---|---|---|
0 | 서울특별시 | 오렌지주유소 | 서울 강동구 성안로 102 (성내동) | SK에너지 | 02-484-6165 | N | - | 1554 | 1354 | 997 |
1 | 서울특별시 | 구천면주유소 | 서울 강동구 구천면로 357 (암사동) | 현대오일뱅크 | 02-441-0536 | N | - | 1556 | 1355 | - |
2 | 서울특별시 | GS칼텍스㈜직영 신월주유소 | 서울 강동구 양재대로 1323 (성내동) | GS칼텍스 | 02-475-2600 | N | 1855 | 1559 | 1349 | 1000 |
3 | 서울특별시 | 광성주유소 | 서울 강동구 올림픽로 673 (천호동) | S-OIL | 02-470-5133 | N | - | 1578 | 1388 | 1100 |
4 | 서울특별시 | (주)소모에너지엔테크놀러지성내주유소 | 서울 강동구 올림픽로 578 (성내동) | GS칼텍스 | 02-479-3838 | Y | - | 1588 | 1388 | - |
stations = pd.DataFrame({'Oil_store': station_raw['상호'],
'주소': station_raw['주소'],
'가격': station_raw['휘발유'],
'셀프': station_raw['셀프여부'],
'상표': station_raw['상표']
})
stations.head()
Oil_store | 주소 | 가격 | 셀프 | 상표 | |
---|---|---|---|---|---|
0 | 오렌지주유소 | 서울 강동구 성안로 102 (성내동) | 1554 | N | SK에너지 |
1 | 구천면주유소 | 서울 강동구 구천면로 357 (암사동) | 1556 | N | 현대오일뱅크 |
2 | GS칼텍스㈜직영 신월주유소 | 서울 강동구 양재대로 1323 (성내동) | 1559 | N | GS칼텍스 |
3 | 광성주유소 | 서울 강동구 올림픽로 673 (천호동) | 1578 | N | S-OIL |
4 | (주)소모에너지엔테크놀러지성내주유소 | 서울 강동구 올림픽로 578 (성내동) | 1588 | Y | GS칼텍스 |
위 처럼 원하는 컬럼만 가지고 오고 이름도 다시 정의해서 stations 라는 변수에 저장하자.
여기에 추가로 주소에서 구 이름만 추출한다. 그래서 구별 주유 가격도 조사하자.
stations['구'] = [eachAddress.split()[1] for eachAddress in stations['주소']]
stations.head()
Oil_store | 주소 | 가격 | 셀프 | 상표 | 구 | |
---|---|---|---|---|---|---|
0 | 오렌지주유소 | 서울 강동구 성안로 102 (성내동) | 1554 | N | SK에너지 | 강동구 |
1 | 구천면주유소 | 서울 강동구 구천면로 357 (암사동) | 1556 | N | 현대오일뱅크 | 강동구 |
2 | GS칼텍스㈜직영 신월주유소 | 서울 강동구 양재대로 1323 (성내동) | 1559 | N | GS칼텍스 | 강동구 |
3 | 광성주유소 | 서울 강동구 올림픽로 673 (천호동) | 1578 | N | S-OIL | 강동구 |
4 | (주)소모에너지엔테크놀러지성내주유소 | 서울 강동구 올림픽로 578 (성내동) | 1588 | Y | GS칼텍스 | 강동구 |
위 출력을 보면 주소 컬럼의 주소를 봤을 때 빈 칸을 기준으로 분리(split) 시키고 두 번째 단어를 선택하면 구 이름이 될 것 같다.
일단 head()만 조사했을 때는 이상 없어 보이지만 5백여 개나 되는 데이터를 다 보기에는 난감하다. 이때는 unique() 검사를 수행하자.
stations['구'].unique()
array(['강동구', '동대문구', '동작구', '마포구', '서대문구', '서초구', '성동구', '서울특별시', '성북구', '송파구', '양천구', '영등포구', '강북구', '용산구', '은평구', '종로구', '중구', '중랑구', '강서구', '관악구', '광진구', '구로구', '금천구', '노원구', '도봉구', '특별시', '강남구'], dtype=object)
stations[stations['구']=='서울특별시']
Oil_store | 주소 | 가격 | 셀프 | 상표 | 구 | |
---|---|---|---|---|---|---|
12 | SK네트웍스(주)효진주유소 | 1 서울특별시 성동구 동일로 129 (성수동2가) | 1654 | N | SK에너지 | 서울특별시 |
서울 특별시를 확인해보니 애초 주소가 입력될 때 알 수 없는 글자가 하나 들어가서 칸 수가 맞지 않았다.
stations.loc[stations['구']=='서울특별시','구'] = '성동구'
stations['구'].unique()
array(['강동구', '동대문구', '동작구', '마포구', '서대문구', '서초구', '성동구', '성북구', '송파구', '양천구', '영등포구', '강북구', '용산구', '은평구', '종로구', '중구', '중랑구', '강서구', '관악구', '광진구', '구로구', '금천구', '노원구', '도봉구', '특별시', '강남구'], dtype=object)
위와 같이 직접 변경하자.
stations[stations['구']=='특별시']
Oil_store | 주소 | 가격 | 셀프 | 상표 | 구 | |
---|---|---|---|---|---|---|
10 | 서현주유소 | 서울 특별시 도봉구 방학로 142 (방학동) | 1524 | Y | S-OIL | 특별시 |
또 특별시로 되었던 것도 검색 했다. 이번엔 서울특별시라 적지 않고 서울과 특별시를 띄어쓰기를 해서 발생한 문제이다.
stations.loc[stations['구']=='특별시','구']= '도봉구'
stations['구'].unique()
array(['강동구', '동대문구', '동작구', '마포구', '서대문구', '서초구', '성동구', '성북구', '송파구', '양천구', '영등포구', '강북구', '용산구', '은평구', '종로구', '중구', '중랑구', '강서구', '관악구', '광진구', '구로구', '금천구', '노원구', '도봉구', '강남구'], dtype=object)
stations[stations['가격']=='-']
Oil_store | 주소 | 가격 | 셀프 | 상표 | 구 | |
---|---|---|---|---|---|---|
18 | 명진석유(주)동서울주유소 | 서울특별시 강동구 천호대로 1456 (상일동) | - | Y | GS칼텍스 | 강동구 |
33 | 하나주유소 | 서울특별시 영등포구 도림로 236 (신길동) | - | N | S-OIL | 영등포구 |
12 | (주)에이앤이청담주유소 | 서울특별시 강북구 도봉로 155 (미아동) | - | Y | SK에너지 | 강북구 |
13 | 송정주유소 | 서울특별시 강북구 인수봉로 185 (수유동) | - | N | 자가상표 | 강북구 |
한 가지 문제가 더 있는데 바로 가격이 기록된 컬럼이 숫자형이 아니라는 것이다. 그래서 확인해 보니 가격이 기록되지 않은 경우 '-' 문자를 기입한 것 같다.
이 주유소들에 대해 우리가 가격을 일일이 확인할 수 없으니 가격 정보가 입력되지 않은 주유소는 대상에서 제외하자.
stations = stations[stations['가격'] != '-']
stations.head()
Oil_store | 주소 | 가격 | 셀프 | 상표 | 구 | |
---|---|---|---|---|---|---|
0 | 오렌지주유소 | 서울 강동구 성안로 102 (성내동) | 1554 | N | SK에너지 | 강동구 |
1 | 구천면주유소 | 서울 강동구 구천면로 357 (암사동) | 1556 | N | 현대오일뱅크 | 강동구 |
2 | GS칼텍스㈜직영 신월주유소 | 서울 강동구 양재대로 1323 (성내동) | 1559 | N | GS칼텍스 | 강동구 |
3 | 광성주유소 | 서울 강동구 올림픽로 673 (천호동) | 1578 | N | S-OIL | 강동구 |
4 | (주)소모에너지엔테크놀러지성내주유소 | 서울 강동구 올림픽로 578 (성내동) | 1588 | Y | GS칼텍스 | 강동구 |
아직 가격 정보가 숫자형으로 변환되지는 않았다.
stations['가격'] = [float(value) for value in stations['가격']]
이제 변수 stations의 가격 컬럼은 float 형으로 변경되었다.
stations.reset_index(inplace=True)
del stations['index']
그리고 25개의 엑셀을 합쳤기 때문에 index가 중복될 수 있다. 그래서 reset_index 명령으로 처음부터 인덱스를 다시 기록하기로 한다. 그러면 index라는 컬럼이 하나 더 생성되는데 그 부분을 제거하자.
stations.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 533 entries, 0 to 532 Data columns (total 6 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 Oil_store 533 non-null object 1 주소 533 non-null object 2 가격 533 non-null float64 3 셀프 533 non-null object 4 상표 533 non-null object 5 구 533 non-null object dtypes: float64(1), object(5) memory usage: 25.1+ KB
이제 데이터가 어느정도 준비 되었다.
4-4 셀프 주유소는 정말 저렴한지 boxplot으로 확인하기¶
앞서 우리는 셀프 주유소가 정말 저렴한지 확인할 수 있는 데이터를 모두 준비했다. 이번엔 아주 간단하게 그래프를 통해 셀프 주유소가 저렴한지 확인하자.
# import platform
import matplotlib.pyplot as plt
import seaborn as sns
%matplotlib inline
import platform
from matplotlib import font_manager, rc
plt.rcParams['axes.unicode_minus'] = False
if platform.system() == 'Darwin':
rc('font', family='AppleGothic')
elif platform.system() == 'Windows':
path = "c:/Windows/Fonts/malgun.ttf"
font_name = font_manager.FontProperties(fname=path).get_name()
rc('font', family=font_name)
elif platform.system() == 'Linux':
path = "/usr/share/fonts/NanumGothic.ttf"
font_name = font_manager.FontProperties(fname=path).get_name()
plt.rc('font', family=font_name)
else:
print('Unknown system... sorry~~~~')
한글 문제를 해결하는 코드를 준비한다.
stations.boxplot(column='가격', by='셀프', figsize=(12,8));
boxplot으로 간편하게 셀프 컬럼을 기준으로 가격 분포를 확인할 수 있게 되었다. 전반적으로 셀프 주유소인 경우가 가격이 낮게 되어있다.
plt.figure(figsize=(12,8))
sns.boxplot(x="상표", y="가격", hue="셀프", data=stations, palette="Set3")
plt.show()
현대 오일뱅크,GS칼텍스,S-Oil,SK에너지 모두 셀프주유소가 저렴하다. SK에너지는 그중 가격대가 가장 높게 형성되어 있다.
plt.figure(figsize=(12,8))
sns.boxplot(x="상표",y="가격", data=stations, palette="Set3")
sns.swarmplot(x="상표", y="가격", data=stations, color=".6")
plt.show()
C:\Users\이재윤\AppData\Roaming\Python\Python39\site-packages\seaborn\categorical.py:1296: UserWarning: 5.6% of the points cannot be placed; you may want to decrease the size of the markers or use stripplot. warnings.warn(msg, UserWarning) C:\Users\이재윤\AppData\Roaming\Python\Python39\site-packages\seaborn\categorical.py:1296: UserWarning: 5.5% of the points cannot be placed; you may want to decrease the size of the markers or use stripplot. warnings.warn(msg, UserWarning)
Swarmplot을 같이 그려보면 좀 더 확실히 데이터의 분포를 볼 수 있다. 셀프 주유소 말고 상표별 데이터를 확인했는데 SK에너지가 높은 가격대를 형성하는 주유소가 많았다. 전반적으로 현대오일뱅크가 4대 브랜드 중에서는 저렴하다는 것을 알 수 있다. 이렇게 해서 셀프 주유소는 대체로 저렴하다고 얘기할 수 있다. 여기서 더 나아가 서울시 구별 주유 가격, 서울에서 높은 가격의 주유손 낮은 가격의 주유소에 대해서도 확인해보자.
4-5 서울시 구별 주유 가격 확인하기¶
import json
import folium
import googlemaps
import warnings
warnings.simplefilter(action = "ignore", category = FutureWarning)
먼저 지도를 구하기 위해 필요한 모듈을 import를 한다. 이제 서울시에서 가장 주유 가격이 비싼 주유소를 보자
stations.sort_values(by='가격', ascending=False).head(10)
Oil_store | 주소 | 가격 | 셀프 | 상표 | 구 | |
---|---|---|---|---|---|---|
325 | 서남주유소 | 서울 중구 통일로 30 (봉래동1가) | 2132.0 | N | SK에너지 | 중구 |
324 | 장충주유소 | 서울 중구 장충단로 202 (장충동1가) | 2130.0 | N | SK에너지 | 중구 |
124 | 양재주유소 | 서울 서초구 바우뫼로 178 (양재동) | 2128.0 | N | SK에너지 | 서초구 |
532 | 뉴서울(강남) | 서울 강남구 언주로 716 (논현동) | 2120.0 | N | SK에너지 | 강남구 |
531 | 오천주유소 | 서울 강남구 봉은사로 503 (삼성동) | 2107.0 | N | SK에너지 | 강남구 |
313 | 재동주유소 | 서울특별시 종로구 율곡로 58 (경운동) | 2106.0 | N | SK에너지 | 종로구 |
285 | 청파주유소 | 서울특별시 용산구 청파로 311 (청파동1가) | 2106.0 | N | SK에너지 | 용산구 |
284 | 갈월동주유소 | 서울특별시 용산구 한강대로 322 (갈월동) | 2106.0 | N | SK에너지 | 용산구 |
283 | 강변주유소 | 서울특별시 용산구 원효로 9 (청암동) | 2106.0 | N | SK에너지 | 용산구 |
323 | 통일주유소 | 서울 중구 동호로 296 (장충동2가) | 2106.0 | N | SK에너지 | 중구 |
stations.sort_values(by='가격', ascending=True).head(10)
Oil_store | 주소 | 가격 | 셀프 | 상표 | 구 | |
---|---|---|---|---|---|---|
83 | 만남의광장주유소 | 서울 서초구 양재대로12길 73-71 (원지동) | 1490.0 | N | 알뜰(ex) | 서초구 |
344 | 강서오곡셀프주유소 | 서울특별시 강서구 벌말로 254 (오곡동) | 1497.0 | Y | SK에너지 | 강서구 |
451 | 태릉솔밭주유소 | 서울특별시 노원구 노원로 49 (공릉동) | 1497.0 | Y | S-OIL | 노원구 |
258 | 수유동주유소 | 서울특별시 강북구 도봉로 395 (수유동) | 1498.0 | Y | GS칼텍스 | 강북구 |
227 | (주)강서오일 | 서울 영등포구 도신로 151 (도림동) | 1499.0 | N | 현대오일뱅크 | 영등포구 |
225 | 도림주유소 | 서울 영등포구 도림로 343 (도림동) | 1499.0 | Y | 알뜰주유소 | 영등포구 |
226 | (주)대청에너지 대청주유소 | 서울 영등포구 가마산로 328 (대림동) | 1499.0 | N | GS칼텍스 | 영등포구 |
415 | 풀페이주유소 | 서울특별시 구로구 경인로 41 (온수동) | 1499.0 | N | SK에너지 | 구로구 |
326 | 신일셀프주유소 | 서울 중랑구 상봉로 58 (망우동) | 1499.0 | Y | SK에너지 | 중랑구 |
40 | 서경주유소 | 서울 동작구 대림로 46 (신대방동) | 1499.0 | N | 현대오일뱅크 | 동작구 |
위에는 서울시에서 가장 낮은 주유 가격의 주유소이다.
pivot_table을 이용해 구별 가격 정보로 변경하고 가격은 평균값으로 정리하자.
import numpy as np
gu_data = pd.pivot_table(stations, index=['구'], values=['가격'],
aggfunc=np.mean)
gu_data.head()
가격 | |
---|---|
구 | |
강남구 | 1791.000000 |
강동구 | 1656.722222 |
강북구 | 1534.333333 |
강서구 | 1576.428571 |
관악구 | 1603.055556 |
geo_path = 'C:/Users/이재윤/DataScience/data/02. skorea_municipalities_geo_simple.json'
geo_str = json.load(open(geo_path, encoding='utf-8'))
map = folium.Map(location=[37.5502, 126.982], zoom_start=10.5,
tiles='Stamen Toner')
map.choropleth(geo_data = geo_str,
data = gu_data,
columns=[gu_data.index, '가격'],
fill_color='PuRd', #PuRd, YlGnBu
key_on='feature.id')
map
이를 서울시 구별 정보에 대해 지도로 표현하자.
4-6 서울시 주유 가격 상하위 10개 주유소 지도에 표현하기¶
주유 가격 상위 10개 주유소를 oil_price_top10 이름으로 저장하자.
oil_price_top10 = stations.sort_values(by='가격', ascending=False).head(10)
oil_price_top10
Oil_store | 주소 | 가격 | 셀프 | 상표 | 구 | |
---|---|---|---|---|---|---|
325 | 서남주유소 | 서울 중구 통일로 30 (봉래동1가) | 2132.0 | N | SK에너지 | 중구 |
324 | 장충주유소 | 서울 중구 장충단로 202 (장충동1가) | 2130.0 | N | SK에너지 | 중구 |
124 | 양재주유소 | 서울 서초구 바우뫼로 178 (양재동) | 2128.0 | N | SK에너지 | 서초구 |
532 | 뉴서울(강남) | 서울 강남구 언주로 716 (논현동) | 2120.0 | N | SK에너지 | 강남구 |
531 | 오천주유소 | 서울 강남구 봉은사로 503 (삼성동) | 2107.0 | N | SK에너지 | 강남구 |
313 | 재동주유소 | 서울특별시 종로구 율곡로 58 (경운동) | 2106.0 | N | SK에너지 | 종로구 |
285 | 청파주유소 | 서울특별시 용산구 청파로 311 (청파동1가) | 2106.0 | N | SK에너지 | 용산구 |
284 | 갈월동주유소 | 서울특별시 용산구 한강대로 322 (갈월동) | 2106.0 | N | SK에너지 | 용산구 |
283 | 강변주유소 | 서울특별시 용산구 원효로 9 (청암동) | 2106.0 | N | SK에너지 | 용산구 |
323 | 통일주유소 | 서울 중구 동호로 296 (장충동2가) | 2106.0 | N | SK에너지 | 중구 |
역시 하위 10개에 대해서도 oil_price_bottom10에 저장하자
oil_price_bottom10 = stations.sort_values(by='가격', ascending=True).head(10)
oil_price_bottom10
Oil_store | 주소 | 가격 | 셀프 | 상표 | 구 | |
---|---|---|---|---|---|---|
83 | 만남의광장주유소 | 서울 서초구 양재대로12길 73-71 (원지동) | 1490.0 | N | 알뜰(ex) | 서초구 |
344 | 강서오곡셀프주유소 | 서울특별시 강서구 벌말로 254 (오곡동) | 1497.0 | Y | SK에너지 | 강서구 |
451 | 태릉솔밭주유소 | 서울특별시 노원구 노원로 49 (공릉동) | 1497.0 | Y | S-OIL | 노원구 |
258 | 수유동주유소 | 서울특별시 강북구 도봉로 395 (수유동) | 1498.0 | Y | GS칼텍스 | 강북구 |
227 | (주)강서오일 | 서울 영등포구 도신로 151 (도림동) | 1499.0 | N | 현대오일뱅크 | 영등포구 |
225 | 도림주유소 | 서울 영등포구 도림로 343 (도림동) | 1499.0 | Y | 알뜰주유소 | 영등포구 |
226 | (주)대청에너지 대청주유소 | 서울 영등포구 가마산로 328 (대림동) | 1499.0 | N | GS칼텍스 | 영등포구 |
415 | 풀페이주유소 | 서울특별시 구로구 경인로 41 (온수동) | 1499.0 | N | SK에너지 | 구로구 |
326 | 신일셀프주유소 | 서울 중랑구 상봉로 58 (망우동) | 1499.0 | Y | SK에너지 | 중랑구 |
40 | 서경주유소 | 서울 동작구 대림로 46 (신대방동) | 1499.0 | N | 현대오일뱅크 | 동작구 |
gmap_key = "AIzaSyCkSfQUN2VCuFMbYkgJ7UpRz4NJGpgD-uU"
gmaps = googlemaps.Client(key=gmap_key)
from tqdm import tqdm_notebook
lat = []
lng = []
for n in tqdm_notebook(oil_price_top10.index):
try:
tmp_add = str(oil_price_top10['주소'][n]).split('(')[0]
tmp_map = gmaps.geocode(tmp_add)
tmp_loc = tmp_map[0].get('geometry')
lat.append(tmp_loc['location']['lat'])
lng.append(tmp_loc['location']['lng'])
except:
lat.append(np.nan)
lng.append(np.nan)
print('Here is nan !')
oil_price_top10['lat']=lat
oil_price_top10['lng']=lng
oil_price_top10
<ipython-input-66-dc0998aef091>:6: TqdmDeprecationWarning: This function will be removed in tqdm==5.0.0 Please use `tqdm.notebook.tqdm` instead of `tqdm.tqdm_notebook` for n in tqdm_notebook(oil_price_top10.index):
Oil_store | 주소 | 가격 | 셀프 | 상표 | 구 | lat | lng | |
---|---|---|---|---|---|---|---|---|
325 | 서남주유소 | 서울 중구 통일로 30 (봉래동1가) | 2132.0 | N | SK에너지 | 중구 | 37.558348 | 126.972090 |
324 | 장충주유소 | 서울 중구 장충단로 202 (장충동1가) | 2130.0 | N | SK에너지 | 중구 | 37.562719 | 127.007010 |
124 | 양재주유소 | 서울 서초구 바우뫼로 178 (양재동) | 2128.0 | N | SK에너지 | 서초구 | 37.478071 | 127.039610 |
532 | 뉴서울(강남) | 서울 강남구 언주로 716 (논현동) | 2120.0 | N | SK에너지 | 강남구 | 37.517636 | 127.035756 |
531 | 오천주유소 | 서울 강남구 봉은사로 503 (삼성동) | 2107.0 | N | SK에너지 | 강남구 | 37.513458 | 127.053786 |
313 | 재동주유소 | 서울특별시 종로구 율곡로 58 (경운동) | 2106.0 | N | SK에너지 | 종로구 | 37.576388 | 126.985851 |
285 | 청파주유소 | 서울특별시 용산구 청파로 311 (청파동1가) | 2106.0 | N | SK에너지 | 용산구 | 37.547451 | 126.969904 |
284 | 갈월동주유소 | 서울특별시 용산구 한강대로 322 (갈월동) | 2106.0 | N | SK에너지 | 용산구 | 37.546955 | 126.972218 |
283 | 강변주유소 | 서울특별시 용산구 원효로 9 (청암동) | 2106.0 | N | SK에너지 | 용산구 | 37.534910 | 126.945869 |
323 | 통일주유소 | 서울 중구 동호로 296 (장충동2가) | 2106.0 | N | SK에너지 | 중구 | 37.560714 | 127.003177 |
이제 주유 가격 상위 10개 주유소에 대해 위도, 경도 정보를 읽어온다. 혹시 알 수 없는 문제, 이를테면 구글맵에서 주소를 검색할 수 없다든지 하는 문제로 에러가 나는 것에 대비해서 try - except 구문을 사용했다. try 구문을 실행하다가 에러가 나면 except 구문에서 지정된 코드를 실행하게 되는데 이 경우는 NaN을 저장하도록 했다.
from tqdm import tqdm
lat = []
lng = []
for n in tqdm(oil_price_bottom10.index):
try:
tmp_add = str(oil_price_bottom10['주소'][n]).split('(')[0]
tmp_map = gmaps.geocode(tmp_add)
tmp_loc = tmp_map[0].get('geometry')
lat.append(tmp_loc['location']['lat'])
lng.append(tmp_loc['location']['lng'])
except:
lat.append(np.nan)
lng.append(np.nan)
print('Here is nan !')
oil_price_bottom10['lat']=lat
oil_price_bottom10['lng']=lng
oil_price_bottom10
100%|██████████████████████████████████████████████████████████████████████████████████| 10/10 [00:02<00:00, 4.75it/s]
Oil_store | 주소 | 가격 | 셀프 | 상표 | 구 | lat | lng | |
---|---|---|---|---|---|---|---|---|
83 | 만남의광장주유소 | 서울 서초구 양재대로12길 73-71 (원지동) | 1490.0 | N | 알뜰(ex) | 서초구 | 37.459906 | 127.042060 |
344 | 강서오곡셀프주유소 | 서울특별시 강서구 벌말로 254 (오곡동) | 1497.0 | Y | SK에너지 | 강서구 | 37.555284 | 126.768567 |
451 | 태릉솔밭주유소 | 서울특별시 노원구 노원로 49 (공릉동) | 1497.0 | Y | S-OIL | 노원구 | 37.625918 | 127.085533 |
258 | 수유동주유소 | 서울특별시 강북구 도봉로 395 (수유동) | 1498.0 | Y | GS칼텍스 | 강북구 | 37.642140 | 127.029568 |
227 | (주)강서오일 | 서울 영등포구 도신로 151 (도림동) | 1499.0 | N | 현대오일뱅크 | 영등포구 | 37.509969 | 126.908231 |
225 | 도림주유소 | 서울 영등포구 도림로 343 (도림동) | 1499.0 | Y | 알뜰주유소 | 영등포구 | 37.507656 | 126.900191 |
226 | (주)대청에너지 대청주유소 | 서울 영등포구 가마산로 328 (대림동) | 1499.0 | N | GS칼텍스 | 영등포구 | 37.498556 | 126.895791 |
415 | 풀페이주유소 | 서울특별시 구로구 경인로 41 (온수동) | 1499.0 | N | SK에너지 | 구로구 | 37.491009 | 126.826675 |
326 | 신일셀프주유소 | 서울 중랑구 상봉로 58 (망우동) | 1499.0 | Y | SK에너지 | 중랑구 | 37.590907 | 127.093834 |
40 | 서경주유소 | 서울 동작구 대림로 46 (신대방동) | 1499.0 | N | 현대오일뱅크 | 동작구 | 37.487947 | 126.908240 |
동일하게 주유 가격이 가장 낮은 10개 주유소에 대해서도 작업을 수행했다.
map = folium.Map(location=[37.5202, 126.975], zoom_start=10.5)
for n in oil_price_top10.index:
if pd.notnull(oil_price_top10['lat'][n]):
folium.CircleMarker([oil_price_top10['lat'][n], oil_price_top10['lng'][n]],
radius=15, color='#CD3181',
fill_color='#CD3181').add_to(map)
for n in oil_price_bottom10.index:
if pd.notnull(oil_price_bottom10['lat'][n]):
folium.CircleMarker([oil_price_bottom10['lat'][n],
oil_price_bottom10['lng'][n]],
radius=15, color='#3186cc',
fill_color='#3186cc').add_to(map)
map
이제 앞 2개의 코드에서 혹시 에러가 나면 NaN으로 처리한 것에 대해 notnull 명령을 이용해서 Nan이 아닐 때만 지도에 표기하도록 한다. 결과를 보면 대체로 중구, 중랑구에 비싼 주유소가 몰려있고 구로구에 저렴한 주유소가 몰려있다는 것을 알 수 있다.
출처 : 파이썬으로 데이터 주무르기
'파이썬으로 데이터 주무르기' 카테고리의 다른 글
파이썬으로 데이터 주무르기 ch.6 (0) | 2021.04.05 |
---|---|
파이썬으로 데이터 주무르기 ch.5 (0) | 2021.04.04 |
파이썬으로 데이터 주무르기 ch.3-2 (0) | 2021.04.02 |
파이썬으로 데이터 주무르기 ch.3-1 (0) | 2021.04.01 |
파이썬으로 데이터 주무르기 ch.2-2 (0) | 2021.03.31 |