EDA 과제2 | 셀프 주유소가 정말 저렴한가, 주유소 데이터분석과제

소리·2023년 11월 17일
0
🔎 LEARNING POINT🔎
- tqdm.notebook을 활용할 수 있게 됨
- 웹크롤링으로 다양한 정보를 추출한 경험
- python : replace, astype 습득
- 그래프를 통해 확인하고 싶은 결과값 확인(swarmplot)
- CSV 내보내기

오늘은 내 풀이와 보완한 풀이를 모두 담아 비교한다.

문제1) 수집한 데이터를 pandas dataframe으로 정리하고, 부가 정보 데이터는 셀프여부와 마찬가지로 Y또는 N으로 저장한다.

주유소명, 주소, 브랜드, 휘발유 가격, 경유 가격,
셀프 여부, 세차장 여부, 충전소 여부, 경정비 여부, 편의점 여부, 24시간 운영 여부, 구, 위도, 경도

[ MY ]

import pandas as pd
from bs4 import BeautifulSoup
import requests
import time
from selenium import webdriver
from selenium.webdriver.common.by import By

각각 필요한 정보를 넣을 리스트를 만든 후 데이터를 삽입하는 방식으로 풀었다.

rom tqdm.notebook import tqdm
import numpy as np

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

time.sleep(2)

title = [] #주유소명
address = [] #주소
brand = [] #주유소 브랜드
ga_price = [] #휘발유 가격
die_price = [] #경유 가격
self_yn = [] #셀프 여부
wash_yn = [] #세차장 여부
charge_yn = [] #충전소 여부
fix_yn = [] #경정비 여부
conv_yn = [] #편의점 여부
allday_yn = [] #24시간 운영여부
gu = [] #구 (주소)
lat = np.NaN #위도 
lng = np.NaN #경도


#지역 선택하기 > 서울
sido_nmo_btn = driver.find_element(By.CSS_SELECTOR, '#SIDO_NM0')
sido_nmo_btn.click() #서울이 있는 목록 클릭
sido_list = sido_nmo_btn.find_elements(By.TAG_NAME, 'option')
sido_list[1].click() #서울 클릭

time.sleep(2)

#시군구 선택하기
sigungu_btn = driver.find_element(By.ID, "SIGUNGU_NM0")
sigungu_btn.click()
sigungu_list = sigungu_btn.find_elements(By.TAG_NAME, 'option') #구 리스트


for i in tqdm(range(1, len(sigungu_list))):
    sigungu_list = driver.find_element(By.ID, "SIGUNGU_NM0").find_elements(By.TAG_NAME, 'option')
    sigungu_list[i].click()
    time.sleep(1)
    
    search_len = driver.find_element(By.ID, "totCnt").text #총 갯수 뽑아내기

    for j in range(1, int(search_len)+1):
        each_station_btn = driver.find_element(By.CSS_SELECTOR, f"#body1 > tr:nth-child({j}) > td.rlist > a")
        each_station_btn.click()
    
        html = driver.page_source
        soup = BeautifulSoup(html, "html.parser")
        
    
        #웹페이지 내용 가져오기
        #제목
        indiviual_title = soup.select_one('#os_nm').get_text()
        title.append(indiviual_title)

        #주소
        indiviual_add = soup.select_one('#rd_addr').get_text()
        address.append(indiviual_add)
        
        #구
        ind_gu = indiviual_add.split(" ")[1]
        gu.append(ind_gu)

        #브랜드
        indiviual_bra = soup.select_one('#poll_div_nm').get_text()
        brand.append(indiviual_bra)

        #휘발유
        indiviual_ga = soup.select_one('#b027_p').get_text()
        ga_price.append(indiviual_ga)

        #경유
        indiviual_die = soup.select_one('#d047_p').get_text()
        die_price.append(indiviual_die)
        
        #셀프
        self_icon = soup.select_one('#self_icon')
        if self_icon and self_icon['id'] == 'self_icon':
            self_yn.append('Y')
        else:
            self_yn.append('N')
            

        #세차장
        if 'off' in soup.select_one('#cwsh_yn')['src']:
            wash_yn.append('N')

        else:
            wash_yn.append('Y')

        #충전소
        if 'off' in soup.select_one('#lpg_yn')['src']:
            charge_yn.append('N')

        else:
            charge_yn.append('Y')

        #경정비 여부
        if 'off' in soup.select_one('#maint_yn')['src']:
            fix_yn.append('N')

        else:
            fix_yn.append('Y')


        #편의점
        if 'off' in soup.select_one('#cvs_yn')['src']:
            conv_yn.append('N')

        else:
            conv_yn.append('Y')

        #24시간
        if 'off' in soup.select_one('#sel24_yn')['src']:
            allday_yn.append('N')

        else:
            allday_yn.append('Y')
        



df = pd.DataFrame({'주유소명' : title,
                  '주소' : address,
                  '브랜드' : brand,
                  '휘발유 가격' : ga_price,
                  '경유 가격' : die_price,
                   '셀프 여부' : self_yn,
                   '세차장 여부' : wash_yn,
                   '충전소 여부' : charge_yn,
                   '경정비 여부' : fix_yn,
                   '편의점 여부' : conv_yn,
                   '24시간 운영여부' : allday_yn,
                   '구' : gu,
                   '위도' : lat,
                   '경도' : lng
                  })

df

🤔 구해야할 값이 많아서 이것저것 구해보고 시도하느라 시간이 많이 소요됐고, 특히 each_station_btn에서#body1 > tr:nth-child({j}) > td.rlist > a 여기를 결과값이 나오게 정리하는 과정이 힘들었다ㅠㅠ
셀프임을 얻는 부분에서 한번에 알지 못해서 힌트가 필요했다. (한 번 이런 방식이 있다는 걸 알았으니까 다음 번에 기억해서 풀어나갈 수 있겠지)
.

  • 경도, 위도 위치를 가져오기; 스타벅스 과제에서 사용한 풀이 활용
import folium
import json
import googlemaps

gmaps_key = 'AIzaSyCiHXsM5pgR02fjYgLbDPhqE3Je4p1PBQg '
gmaps = googlemaps.Client(key=gmaps_key)

m = folium.Map(location = [37.56653641975829, 126.98185562167849], zoom_start = 12)
m

for idx, rows in tqdm(enumerate(df['주소'])):
    tmp = gmaps.geocode(rows, language='ko')

    if tmp:
        lat = tmp[0].get('geometry')['location']['lat']
        lng = tmp[0].get('geometry')['location']['lng']
        df.loc[idx, '위도'] = lat
        df.loc[idx, '경도'] = lng
        
    else:
        print(f'에러 지점 : {idx}, {rows}')

driver.quit()

df

[ OUT ]

	주유소명	주소	브랜드	휘발유 가격	경유 가격	셀프 여부	세차장 여부	충전소 여부	경정비 여부	편의점 여부	24시간 운영여부	구	위도	경도
0	HD현대오일뱅크㈜직영 도곡셀프주유소	서울 강남구 남부순환로 2718 (도곡2동)	HD현대오일뱅크	1,784	1,719	Y	Y	N	Y	N	N	강남구	37.485462	127.043218
1	(주)중앙에너비스 수서지점	서울 강남구 광평로 202 (수서동)	SK에너지	1,787	1,737	Y	Y	N	N	N	N	강남구	37.484105	127.095040
2	방죽주유소	서울 강남구 밤고개로 215 (율현동)	GS칼텍스	1,787	1,739	Y	Y	N	N	N	N	강남구	37.474993	127.106887
3	오일프러스 셀프	서울 강남구 남부순환로 2651 (도곡동)	SK에너지	1,794	1,734	Y	Y	N	Y	N	Y	강남구	37.485839	127.039672
4	삼성동주유소	서울 강남구 테헤란로 619 (삼성동)	HD현대오일뱅크	1,795	1,755	Y	Y	N	N	N	N	강남구	37.509858	127.065110
...	...	...	...	...	...	...	...	...	...	...	...	...	...	...
429	신내주유소	서울 중랑구 용마산로 705 (신내동)	SK에너지	1,799	1,698	Y	Y	N	N	N	N	중랑구	37.617125	127.095896
430	(주)기지에너지	서울 중랑구 용마산로 716 (신내동)	S-OIL	1,799	1,699	N	N	N	N	N	N	중랑구	37.617628	127.094381
431	범아주유소	서울 중랑구 동일로 881 (묵동)	S-OIL	1,819	1,759	N	Y	N	Y	N	N	중랑구	37.609311	127.077673
432	용마로주유소	서울 중랑구 용마산로 309 (면목동)	SK에너지	1,838	1,738	Y	Y	N	N	N	N	중랑구	37.579878	127.092182
433	신일셀프주유소	서울 중랑구 상봉로 58 (망우동)	SK에너지	1,849	1,748	Y	Y	N	Y	N	Y	중랑구	37.590942	127.093807
434 rows × 14 columns

CSV 파일로 내보내기

#결측치 있는지 확인
df.isnull().sum()

#내보내기
df.to_csv('./seoul_oil_station.csv', sep = ',' , encoding = 'utf-8')

#파일 확인하기
df2 = pd.read_csv('./seoul_oil_station.csv', index_col = 0, encoding = 'utf-8')
df.head(5)

[ 다른 풀이]

  • 서울시와 구 이름까지 추출하기
import pandas as pd
from selenium import webdriver
from selenium.webdriver.common.by import By
from bs4 import BeautifulSoup
import time
import numpy as np

#1. 페이지 접근
url = 'https://www.opinet.co.kr/searRgSelect.do'
driver = webdriver.Chrome()
driver.get(url)

#2. 서울 고정
#seoul = driver.find_elements(By.CSS_SELECTOR, '#SIDO_NM0 > option') 이렇게 할 수 있으나 하지 않는 이유는 뒷 쪽에서 send_keys를 해서 값을 보내야 하기 때문이다/

seoul = driver.find_element(By.CSS_SELECTOR, '#SIDO_NM0')
sido_list = seoul.find_elements(By.CSS_SELECTOR, 'option')
sido_names = [option.get_attribute('value') for option in sido_list]
print(sido_names) #맨 앞이 빈 칸이므로

sido_names = sido_names[1:]
seoul.send_keys(sido_names[0]) #서울특별시 클릭

#3. 구별 선택
gu_list_raw = driver.find_element(By.CSS_SELECTOR, '#SIGUNGU_NM0') #부모태그
gu_list = gu_list_raw.find_elements(By.CSS_SELECTOR, 'option')
gu_names = [option.get_attribute('value') for option in gu_list if option.get_attribute('value')] #공백 제거 맨 앞이 value값이 없음

print(gu_names)
  • 크롤링 및 데이터 프레임 만들기
import googlemaps
gmaps_key = 'AIzaSyCiHXsM5pgR02fjYgLbDPhqE3Je4p1PBQg '
gmaps = googlemaps.Client(key=gmaps_key)

datas = []

for i in range(len(gu_names)):
    gu_selector = f'#SIGUNGU_NM0 > option:nth-child({i+2})'
    driver.find_element(By.CSS_SELECTOR, gu_selector).click()
    station_items = driver.find_elements(By.CSS_SELECTOR, '#body1 > tr') #주유소 목록
    
    for idx in range(len(station_items)):
        detail_selector = f'#body1 > tr:nth-child({idx+1}) > td.rlist > a'
        driver.find_element(By.CSS_SELECTOR, detail_selector).click()
        
        name = driver.find_element(By.CSS_SELECTOR, '.header #os_nm').get_attribute('innerText')
        gasoline = driver.find_element(By.CSS_SELECTOR, '#b027_p').get_attribute('innerText')
        diesel = driver.find_element(By.CSS_SELECTOR, '#d047_p').get_attribute('innerText')
        address = driver.find_element(By.CSS_SELECTOR, '#rd_addr').get_attribute('innerText')
        brand = driver.find_element(By.CSS_SELECTOR, '#poll_div_nm').get_attribute('innerText')
       
        cwsh_yn = 'N' if '_off' in driver.find_element(By.CSS_SELECTOR, '.service #cwsh_yn').get_attribute('src').split('/')[-1] else 'Y'
        lpg_yn = 'N' if '_off' in driver.find_element(By.CSS_SELECTOR, '.service #lpg_yn').get_attribute('src').split('/')[-1] else 'Y'
        main_yn = 'N' if '_off' in driver.find_element(By.CSS_SELECTOR, '.service #maint_yn').get_attribute('src').split('/')[-1] else 'Y'
        cvs_yn = 'N' if '_off' in driver.find_element(By.CSS_SELECTOR, '.service #cvs_yn').get_attribute('src').split('/')[-1] else 'Y'
        sel24_yn = 'N' if '_off' in driver.find_element(By.CSS_SELECTOR, '.service #sel24_yn').get_attribute('src').split('/')[-1] else 'Y'
        
        try:
            driver.find_element(By.CSS_SELECTOR, '#self_icon').get_attribute('alt')
            is_self = 'Y'
        except:
            is_self = 'N'
        
        gu = address.split()[1]
        
        tmp = gmaps.geocode(address, language = 'ko')
        lat = tmp[0].get('geometry')['location']['lat']
        lng = tmp[0].get('geometry')['location']['lng']

        datas.append({
            'name' : name,
            'address' : address,
            'brand' : brand,
            'is_self' : is_self,
            'gasoline' : gasoline,
            'diesel' : diesel,
            'car_wash' : cwsh_yn,
            'charging_station' : lpg_yn,
            'car_maintenance' : main_yn,
            'convenience_store': cvs_yn,
            '24_hours' : sel24_yn,
            'gu' : gu,
            'lat' : lat,
            'lng' : lng
        })
        time.sleep(0.2)
    time.sleep(0.5)

driver.quit()

df = pd.DataFrame(datas)
df.tail()

❗find_element로 추출했고, comprehension으로 한줄로 마무리. 나는 ['scr']라고 한 걸, get_atrribute('src')로 풀어냈음.

import datetime

now = datetime.datetime.now()
nowDate = now.strftime('%Y%m%d')

df.to_csv(f'./oilstation_oneday_{nowDate}.csv', encoding = 'utf-8')

stations = pd.read_csv('./oilstation_oneday_20231108.csv', encoding = 'utf-8', thousands = ',' ,index_col=0)
#thousands를 해줘야 읽을 때 ,가 들어간 것을 str이 아닌 숫자로 인식!
stations

문제2) 분석한 결과를 바탕으로 휘발유와 경유 가격이 셀프주유소에서 정말 저렴한지 의견을 제시하라

[ MY ]

import matplotlib.pyplot as plt
import seaborn as sns

import matplotlib.pyplot as plt
from matplotlib import font_manager, rc
import warnings
warnings.filterwarnings('ignore')

df2['휘발유 가격'] = df2['휘발유 가격'].apply(lambda x: x.replace(',', '')).astype(int)
df2['경유 가격'] = df2['경유 가격'].apply(lambda x: x.replace(',', '')).astype(int)

❗ 가격에 ,이 있어서 str형식이기에 컴마를 replace로 해서 제거하고 astype으로 형식을 변경

print(df2['휘발유 가격'].sort_values().min())
print(df2['경유 가격'].sort_values().min())

print(df2['휘발유 가격'].sort_values().max())
print(df2['경유 가격'].sort_values().max())

minx = int(df2['경유 가격'].sort_values().min())
maxx = int(df2['경유 가격'].sort_values().max())

f_path = 'C:\Windows\Fonts\malgun.ttf'
font_manager.FontProperties(fname=f_path).get_name()
rc('font', family='Malgun Gothic')

fig, axes = plt.subplots(1, 2, figsize = (15, 5))

axes[0].set(ylim=(minx, maxx))
ax1 = sns.swarmplot(x='셀프 여부', y='휘발유 가격', data=df2, ax = axes[0])
ax1.set_title('휘발유 가격 비교')

axes[1].set(ylim=(minx, maxx))
ax2 = sns.swarmplot(x='셀프 여부', y='경유 가격', data=df2, ax = axes[1])
ax2.set_title('경유 가격 비교')

plt.show()


df2[df2['휘발유 가격'] == df2['휘발유 가격'].sort_values().min()]

df2[df2['경유 가격'] == df2['경유 가격'].sort_values().min()]

[ 다른 풀이 ]

#주유소 정보 시각화

#설정 부분
import matplotlib.pyplot as plt
import seaborn as sns

import matplotlib.pyplot as plt
from matplotlib import font_manager, rc
import warnings
warnings.filterwarnings('ignore')

f_path = 'C:\Windows\Fonts\malgun.ttf'
font_manager.FontProperties(fname=f_path).get_name()
rc('font', family='Malgun Gothic')

#그래프 그리기
plt.figure(figsize = (12, 8))
sns.boxplot(x='is_self', y='gasoline', data=stations, palette='Set1')
plt.grid(True)
plt.show()

#휘발유 브랜드별 비교
plt.figure(figsize = (12, 8))
sns.boxplot(x='brand', y='gasoline', hue='is_self', data=stations, palette='Set1')
plt.grid(True)
plt.show()

#휘발유 구별 비교
plt.figure(figsize = (12, 8))
sns.boxplot(x='gu', y='gasoline', data=stations, palette='Set1')
plt.grid(True)
plt.show()

#경유 브랜드별 비교
plt.figure(figsize = (12, 8))
sns.boxplot(x='brand', y='diesel', hue='is_self', data=stations, palette='Set1')
plt.grid(True)
plt.show()

#경유 구별 비교
plt.figure(figsize = (12, 8))
sns.boxplot(x='gu', y='diesel', data=stations, palette='Set1')
plt.grid(True)
plt.show()

#지도시각화
import json
import folium

#가장 값싼 주유소 10개
stations[['gu', 'name', 'is_self', 'gasoline']].sort_values(by='gasoline', ascending=True).head(10).reset_index(drop=True)

#지도 시각화용 데이터 프레임
import numpy as np
gu_data = pd.pivot_table(data=stations, index = 'gu', values='gasoline', aggfunc=np.mean)

geo_path = '..data/02/ skorea_municipalities_geo_simple.json'
geo_str = json.load(open(geo_path, encoding = 'utf-8'))

m = folium.Map(location = [37.5502, 126.982], zoom_start = 10.5)
m.choropleth(
    geo_data = geo_str,
    data = gu_data,
    columns = [gu_data.index, 'gasoline'],
    key_one = 'feature.id',
    fill_color='PuRd'
)
m

[ MY ]

셀프 주유소가 좀더 저렴한가?

셀프주유소를 선택했을 때 일반 주유소보다 휘발유, 경유 모두 저렴할 확률이 높다. 그래프에서 '셀프 여부'가 'Y'(yes)일 때 'N'(No)보다 하단에 점들이 많이 위치해있다는 것을 확인할 수 있다. 그러나 '경유 가격'의 경우 가장 저렴한 주유소는 셀프 주유소가 아닌 점을 볼 때, 절대적으로 셀프주유소가 일반 주유소보다 싸다고 말하기는 어렵다.

🤔 그래프를 그리는 게 경험이 별로 없어서 어떤 그래프가 적절할 지 생각하는 것부터 코드를 짜는 것까지 시간이 오래걸렸다.

profile
데이터로 경로를 탐색합니다.

0개의 댓글