[제로베이스 데이터 취업 스쿨] 9기 8주차 – EDA2: 셀프 주유소는 정말 저렴한가?

Inhee Kim·2022년 12월 26일
1

Project

목록 보기
2/8
post-thumbnail

다음의 가설이 참이라고 할 수 있는지 분석
가설: 셀프 주유소는 저렴하다.

Requirements & 한글 대응

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import platform
import seaborn as sns
import googlemaps
import time
import folium
import warnings

from urllib.request import urlopen, Request
from bs4 import BeautifulSoup
from selenium import webdriver
from tqdm import tqdm_notebook
from matplotlib import font_manager, rc

warnings.filterwarnings(action='ignore')
plt.rc('axes', unicode_minus = False)
%matplotlib inline


path = 'C:/Windows/Fonts/malgun.ttf'

if platform.system() == 'Darwin':
    rc('font', family = 'Arial Unicode MS')
elif platform.system() == 'Windows':
    font_name = font_manager.FontProperties(fname = path).get_name()
    rc('font', family = font_name)
else:
    print('Unknown system. sorry.')

문제1

서울시의 스타벅스 매장의 이름과 주소, 구 이름을 pandas data frame으로 정리

  • 수집한 데이터들을 pandas data frame으로 정리
  • 부가 정보 데이터는 Y/N로 저장
  • 최종적으로 데이터 프레임에 들어가야할 col은 총 14개
    - 1. 주유소명
    - 2. 주소
    - 3. 브랜드
    - 4. 휘발유 가격
    - 5. 경유 가격
    - 6. 셀프 여부
    - 7. 세차장 여부
    - 8. 충전소 여부
    - 9. 경정비 여부
    - 10. 편의점 여부
    - 11. 24시간 운영 여부
    - 12. 구
    - 13. 위도
    - 14. 경도

1. 주유소 데이터 가져오기 - Selenium 접근

(1) Chrome으로 오피넷 사이트 열기

url = 'https://www.opinet.co.kr/searRgSelect.do'
driver = webdriver.Chrome('../driver/chromedriver.exe')
driver.get(url)

# 주유소 사이트를 한 번 불러오면 opinet 홈페이지가 실행되기 때문에, 한 번 더 요청하여 싼 주유소 찾기 사이트로 이동
time.sleep(1)
driver.get(url)

(2) 지역 검색 > '서울' 선택

sido_select = driver.find_element_by_css_selector('#SIDO_NM0')
sido_select.send_keys('서울')

(3) 부가정보 체크하기(세차장, 경정비, 편의점, 24시간)

driver.find_element_by_css_selector('#CWSH_YN').click()
driver.find_element_by_css_selector('#MAINT_YN').click()
driver.find_element_by_css_selector('#CVS_YN').click()
driver.find_element_by_css_selector('#SEL24_YN').click()

(4) 서울시 '구' 리스트 추출

gu_select = driver.find_element_by_css_selector('#SIGUNGU_NM0')
gu_list = gu_select.find_elements_by_tag_name('option')

gu_names = [gu.get_attribute('value') for gu in gu_list]
gu_names = gu_names[1:]
gu_names, len(gu_names)

2. 데이터 수집 - BeautifulSoup (test)

# 임의 '구' 선택: 마포구
gu_select = driver.find_element_by_css_selector('#SIGUNGU_NM0')
gu_select.send_keys(gu_names[12])  # 마포구
driver.find_element_by_css_selector('#searRgSelect > span').click()

# 조회 버튼 클릭
driver.find_element_by_css_selector('#searRgSelect > span').click()

# soup에 담기
req = driver.page_source
soup = BeautifulSoup(req, 'html.parser')

# 마포구 주유소 개수 확인
oilStation_cnt = int(driver.find_element_by_css_selector('#totCnt').text)
oilStation_cnt

# 임의 주유소명 클릭
driver.find_element_by_css_selector('#body1 > tr:nth-child(1) > td.rlist > a').click()

# soup2에 담기
req2 = driver.page_source
soup2 = BeautifulSoup(req2, 'html.parser')

# 주유소 정보 리스트 확인
oilStation_list = soup2.select('#body1 > tr:nth-child(1) > td.rlist > a')
oilStation_list

# 주유소명, 휘발유 가격, 경유 가격 추출
name = soup2.find(id = 'os_nm').text
gasolinePrice = soup2.find('div', {'class':'gis_detail_info_bcon mgt_15'}).find('label', {'id':'b027_p'}).text
dieselPrice = soup2.find('div', {'class':'gis_detail_info_bcon mgt_15'}).find('label', {'id':'d047_p'}).text

name, gasolinePrice, dieselPrice

# 주소, 구, 브랜드 추출
address = soup2.find(id = 'rd_addr').text
gu = address.split()[1]
brand = soup2.find(id = 'poll_div_nm').text

address, gu, brand

# 부가정보: 이미지 색깔 표시 여부에 따라 Y/N 적용
# 셀프 여부
if soup2.find('tbody').find_all('tr')[0].find('span'):
    if soup2.find('tbody').find_all('tr')[0].find('span').text == '셀프':
        self = 'Y'
    else:
        self = 'N'

# 세차장 여부
if soup2.find(id = 'cwsh_yn').attrs['src'] == '/images/user/gis/oil_station_service1_01.gif':
    wash = 'Y'
else:
    wash = 'N'

# 충전소 여부
if soup2.find(id = 'lpg_yn').attrs['src'] == '/images/user/gis/oil_station_service1_02.gif':
    lpg = 'Y'
else:
    lpg = 'N'

# 경정비 여부
if soup2.find(id = 'maint_yn').attrs['src'] == '/images/user/gis/oil_station_service1_03.gif':
    maint = 'Y'
else:
    maint = 'N'

# 편의점 여부
if soup2.find(id = 'cvs_yn').attrs['src'] == '/images/user/gis/oil_station_service1_04.gif':
    cvs = 'Y'
else:
    cvs = 'N'

# 24시 영업 여부
if soup2.find(id = 'sel24_yn').attrs['src'] == '/images/user/gis/oil_station_service1_05.gif':
    sel24 = 'Y'
else:
    sel24 = 'N'

self, wash, lpg, maint, cvs, sel24

driver.close()

3. 전체 데이터 수집

  • 위에서 수행한 코드를 가지고 전체 데이터(서울시 구별 주유소 데이터) 수집
url = 'https://www.opinet.co.kr/searRgSelect.do'
driver = webdriver.Chrome('../driver/chromedriver.exe')
driver.get(url)
time.sleep(1)
driver.get(url)

sido_select = driver.find_element_by_css_selector('#SIDO_NM0')
sido_select.send_keys('서울')

driver.find_element_by_css_selector('#CWSH_YN').click()
driver.find_element_by_css_selector('#MAINT_YN').click()
driver.find_element_by_css_selector('#CVS_YN').click()
driver.find_element_by_css_selector('#SEL24_YN').click()
gu_select = driver.find_element_by_css_selector('#SIGUNGU_NM0')
gu_list = gu_select.find_elements_by_tag_name('option')

gu_names = [gu.get_attribute('value') for gu in gu_list]
gu_names = gu_names[1:]
gu_names[:5], len(gu_names)
oilStation_df = []

for gu in tqdm_notebook(gu_names):
    
    # 구 선택
    gu_select = driver.find_element_by_css_selector('#SIGUNGU_NM0')
    gu_select.send_keys(gu)
    time.sleep(1)
    
    # 조회 버튼 클릭
    driver.find_element_by_css_selector('#searRgSelect > span').click()
    time.sleep(1)
    
    # soup에 담기
    req = driver.page_source
    soup = BeautifulSoup(req, 'html.parser')
    soup
    
    oilStation_cnt = int(driver.find_element_by_css_selector('#totCnt').text)
    for i in range(1, oilStation_cnt+1):
        
        # 주유소 클릭
        driver.find_element_by_css_selector(f'#body1 > tr:nth-child({i}) > td.rlist > a').click()
        time.sleep(1)
        
        # soup2에 담기
        req2 = driver.page_source
        soup2 = BeautifulSoup(req2, 'html.parser')
        soup2
        
        # 주유소명, 휘발유 가격, 경유 가격
        name = soup2.find(id = 'os_nm').text
        gasolinePrice = soup2.find('div', {'class':'gis_detail_info_bcon mgt_15'}).find('label', {'id':'b027_p'}).text
        dieselPrice = soup2.find('div', {'class':'gis_detail_info_bcon mgt_15'}).find('label', {'id':'d047_p'}).text
        
        # 주소, 구, 브랜드
        address = soup2.find(id = 'rd_addr').text
        gu = address.split()[1]
        brand = soup2.find(id = 'poll_div_nm').text
        
        # 부가정보: Y/N
        # 셀프 여부
        if soup2.find('tbody').find_all('tr')[i-1].find('span'):
            if soup2.find('tbody').find_all('tr')[i-1].find('span').text == '셀프':
                self = 'Y'
            else:
                self = 'N'

        # 세차장 여부
        if soup2.find(id = 'cwsh_yn').attrs['src'] == '/images/user/gis/oil_station_service1_01.gif':
            wash = 'Y'
        else:
            wash = 'N'

        # 충전소 여부
        if soup2.find(id = 'lpg_yn').attrs['src'] == '/images/user/gis/oil_station_service1_02.gif':
            lpg = 'Y'
        else:
            lpg = 'N'

        # 경정비 여부
        if soup2.find(id = 'maint_yn').attrs['src'] == '/images/user/gis/oil_station_service1_03.gif':
            maint = 'Y'
        else:
            maint = 'N'

        # 편의점 여부
        if soup2.find(id = 'cvs_yn').attrs['src'] == '/images/user/gis/oil_station_service1_04.gif':
            cvs = 'Y'
        else:
            cvs = 'N'

        # 24시 영업 여부
        if soup2.find(id = 'sel24_yn').attrs['src'] == '/images/user/gis/oil_station_service1_05.gif':
            sel24 = 'Y'
        else:
            sel24 = 'N'

        datas = {
            'name':name, 
            'gasolinePrice':gasolinePrice,
            'dieselPrice':dieselPrice,
            'address':address,
            'gu':gu,
            'brand':brand,
            'self':self,
            'wash':wash,
            'lpg':lpg,
            'maint':maint,
            'cvs':cvs,
            'sel24':sel24
        }
        oilStation_df.append(datas)
oilStation_df = pd.DataFrame(oilStation_df)
oilStation_df.tail(3)

oilStation_df['gu'].unique(), len(oilStation_df['gu'].unique())

driver.close()
# 위도, 경도 컬럼 추가
gmaps_key = '__________'  # 본인이 발급받은 키값 입력
gmaps = googlemaps.Client(key = gmaps_key)
oilStation_df['lat'] = np.nan
oilStation_df['lng'] = np.nan

for idx, rows in tqdm_notebook(oilStation_df.iterrows()):
    tmp = gmaps.geocode(rows['address'], language = 'ko')
    
    if tmp:
        lat = tmp[0].get('geometry')['location']['lat']
        lng = tmp[0].get('geometry')['location']['lng']
    
        oilStation_df.loc[idx, 'lat'] = lat
        oilStation_df.loc[idx, 'lng'] = lng
    else:
        print(idx, rows['address'])
oilStation_df.tail(3)

# 휘발유와 경유 가격을 float형으로 변환하기 위해 천단위 콤마(,) 제거
for i in range(len(oilStation_df)):
    oilStation_df['gasolinePrice'][i] = float(oilStation_df['gasolinePrice'][i].replace(',',''))
    oilStation_df['dieselPrice'][i] = float(oilStation_df['dieselPrice'][i].replace(',',''))

oilStation_df.head(3)

oilStation_df.info()

콤마를 제거해 주었지만 type은 object
따라서 float로 변환해주고자 한다.

# 휘발유 가격, 경유 가격을 float로 변환
oilStation_df['gasolinePrice'] = oilStation_df['gasolinePrice'].astype('float')
oilStation_df['dieselPrice'] = oilStation_df['dieselPrice'].astype('float')

oilStation_df.info()

oilStation_df.head(3)

oilStation_df.to_csv('../data/EDA2_oil_station_data.csv', sep = ',', encoding = 'utf-8')

문제2

휘발유와 경유 가격이 셀프 주유소에서 정말 저렴한지 분석 결과 작성

  • 분석 결과는 markdown으로 작성할 것

1. 저장한 데이터 불러들이기

df = pd.read_csv('../data/EDA2_oil_station_data.csv', index_col = 0, encoding = 'utf-8')
df.tail(3)

2. 시각화: 휘발유, 경유 가격 비교

시각화에 앞서 휘발유와 경유가 가장 싼 곳과 비싼곳을 순서대로 5군데를 보고자 한다.

# 휘발유 가장 비싼 주유소 5개
df.sort_values(by = 'gasolinePrice', ascending = False).head(5)

# 휘발유 가장 싼 주유소 5개
df.sort_values(by = 'gasolinePrice', ascending = True).head(5)

# 경유 가장 비싼 주유소 5개
df.sort_values(by = 'dieselPrice', ascending = False).head(5)

# 경유 가장 싼 주유소 5개
df.sort_values(by = 'dieselPrice', ascending = True).head(5)

  • 서울시 전체의 휘발유 가격은 1410 ~ 2578원으로 형성되어 있으며, 강남구, 중구, 용산구에서 가장 높은 가격대를 형성하고 있고, 양천구와 강서구에서 가장 낮은 가격대를 형성하고 있다.
  • 서울시 전체의 경유 가격은 1645 ~ 2666원으로 형성되어 있으며, 휘발유와 마찬가지로 강남구, 중구, 용산구에서 가장 높은 가격대를 형성하고 있고, 강서구와 금천구에서 가장 낮은 가격대를 형성하고 있다.

(1) 셀프 여부에 따른 가격 비교

plt.figure(figsize = (12, 8))
sns.set_style('darkgrid')
plt.grid(True)

plt.subplot(121)
sns.boxplot(x = 'self', y = 'gasolinePrice', data = df, palette = 'husl')

plt.subplot(122)
sns.boxplot(x = 'self', y = 'dieselPrice', data = df, palette = 'husl')

plt.show()

  • 휘발유와 경유 모두 셀프 주유소에서의 가격이 더 낮게 형성되어 있음을 알 수 있다.
  • 또한 셀프 주유소가 아닌 곳에서의 가격대는 상대적으로 광범위하게 분포되어 있음을 알 수 있다.

(2) 브랜드에 따른 가격 비교

# 휘발유
plt.figure(figsize = (12, 8))
plt.grid(True)
sns.boxplot(x = 'brand', y = 'gasolinePrice', hue = 'self', data = df, palette = 'husl')
plt.show()

# 경유
plt.figure(figsize = (12, 8))
plt.grid(True)
sns.boxplot(x = 'brand', y = 'dieselPrice', hue = 'self', data = df, palette = 'husl')
plt.show()

  • 브랜드에 따른 휘발유, 경유 가격 그래프를 보면, 셀프 여부에 따른 평균, 중앙값 차이가 거의 모든 케이스에서 셀프 주유소가 더 저렴하다고 볼 수 있다.
  • 그러나 알뜰주유소, 알뜰(ex), 자가상표의 경우 표본 값이 작아 신뢰도가 낮다고 볼 수 있다.
  • 따라서 모든 주유소에서 셀프가 더 저렴하다 라는 결론보다는 '대부분의 주유소에서 셀프가 더 저렴하다.'고 할 수 있다.

(3) '구'에 따른 가격 비교

# 휘발유
plt.figure(figsize = (20, 10))
plt.grid(True)
sns.boxplot(x = 'gu', y = 'gasolinePrice', hue = 'self', data = df, palette = 'husl')
plt.show()

# 경유
plt.figure(figsize = (20, 10))
plt.grid(True)
sns.boxplot(x = 'gu', y = 'dieselPrice', hue = 'self', data = df, palette = 'husl')
plt.show()

  • '구'에 따른 휘발유, 경유 가격 그래프를 보면,
  • 셀프 여부에 따른 평균, 중앙값 차이가 거의 모든 케이스에서 셀프주유소가 더 저렴하다고 볼 수 있다.
  • 그러나, 강남구, 강동구, 서초구의 셀프주유소의 중앙값은 강서구, 관악구, 구로구, 금천구 등의 몇몇 다른 구의 보통주유소보다 가격이 높게 형성되어 있을을 확인하였다.

결론

  • 셀프여부와 브랜드에 따른 그래프를 참고하면 셀프주유소의 휘발유, 경유 가격은 보통주유소의 가격보다 낮게 형성되어 있는 것은 사실이다.

  • 그러나, '구'별로 보았을 때는 해석이 달리 된다.

  • 각 구에서 셀프주유소의 가격은 보통주유소보다 낮게 형성되어 있다고 볼 수 있으나,

  • 특정 구(강남구, 강동구, 서초구)의 셀프주유소에서의 휘발유, 경유 가격은 특정 구(강서구, 관악구, 구로구, 금천구 등)의 보통주유소에서의 가격보다 높게 형성되어 있음을 확인할 수 있었다.

  • 따라서 모든 셀프주유소의 휘발유, 경유 가격은 보통주유소보다 저렴하지는 않지만, 대체로 저렴하다고 볼 수 있다.

profile
Date Scientist & Data Analyst

0개의 댓글