🤔 서울시 소재 셀프 주유소는 비셀프 주유소에 비해 더 저렴할까? (가설)🤔
셀프 주유소는 직원의 서비스를 요구하지 않으므로 더 저렴할 거 같다.
진짜 그런지 서울에 있는 주유소 정보들을 opinet에서 scrapping한 후, 여러가지 기법으로 증명해보자!
한국석유공사 오피넷 - 이동하기
내가 원하는 지역의 주유소 목록과 주유 정보를 보기 위해서는 어떤 과정을 거쳐야 하는가?
아래의 코드는 필요한 라이브러리를 이미 임포트했고, Selenium을 통해 타겟 웹 페이지에 접속했다는 가정 하의 코드임
✔️ '서울' 선택(고정) 및 서울 내 구 리스트 생성
seoul_button = wait.until(EC.element_to_be_clickable((By.XPATH, '//*[@id="SIDO_NM0"]/option[2]'))).click() html = driver.page_source soup = BeautifulSoup(html, "html.parser") gu_list = [option.text for option in soup.find('select', {'id': 'SIGUNGU_NM0'}).find_all('option')[1:]]
프린트 결과)
['강남구', '강동구', '강북구', '강서구', '관악구', '광진구', '구로구', '금천구',
'노원구', '도봉구', '동대문구', '동작구', '마포구', '서대문구', '서초구', '성동구',
'성북구', '송파구', '양천구', '영등포구', '용산구', '은평구', '종로구', '중구', '중랑구']
✔️ 정보 스크래핑 자동화 코드
oil_station_info = [] for gu_name in tqdm(gu_list): # 구 선택 gu_selector = f'//*[@id="SIGUNGU_NM0"]/option[@value="{gu_name}"]' wait.until(EC.element_to_be_clickable((By.XPATH, gu_selector))).click() # 구 클릭 stations = driver.find_elements_by_css_selector('#body1 > tr') # 주유소 목록 추출 totCnt = driver.find_element_by_css_selector("#totCnt").text # '검색 결과' 옆 주유소 개수가 gu_totalCnt = driver.find_elements_by_css_selector("#body1 > tr") # '검색 결과'에 나오는 테이블(주유소) 수와 일치하는지 확인 print(gu_name, totCnt, len(gu_totalCnt)) wait.until(lambda d: d.execute_script('return document.readyState') == 'complete') # 페이지 갱신 대기 for idx in tqdm(range(len(stations))): 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') # 경유 가격 brand = driver.find_element_by_css_selector('#poll_div_nm').get_attribute('innerText') # 주유소 브랜드 car_wash = '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' # 충전소 유무 maint_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' # 24시간 영업 유무 try: driver.find_element_by_css_selector('#self_icon').get_attribute('alt') is_self = 'Y' # 셀프 주유 여부 except: is_self = 'N' # address address = driver.find_element_by_css_selector('#rd_addr').get_attribute('innerText') # 주유소 주소 # gu gu = address.split()[1] # 어느 구에 해당 주유소가 있는지 # lat, lng tmp = gmaps.geocode(address, language='ko') lat = tmp[0].get('geometry')['location']['lat'] # 주유소 위도 lng = tmp[0].get('geometry')['location']['lng'] # 주유소 경도 oil_station_info.append({ 'name': name, 'address': address, 'brand': brand, 'is_self': is_self, 'gasoline': gasoline, 'diesel': diesel, 'car_wash': car_wash, 'charging_station': lpg_yn, 'car_maintenance': maint_yn, 'cvs': cvs_yn, '24_hour': sel24_yn, 'gu': gu, 'lat': lat, 'lng': lng }) print(f'{gu} - {idx}. {name} // 완료') time.sleep(0.5) print('=====================================') print() time.sleep(0.5) print('******* 전체 주유소 스크래핑 완료 *******') driver.quit() df = pd.DataFrame(oil_station_info) df.tail()
프린트 결과 생략
df['lat'].min(), df['lat'].max() # 위도 이상치 x df['lng'].min(), df['lng'].max() # 경도 이상치 x df.columns df.info() df.to_csv('./서울_구별_주유소_정보.csv', encoding='utf-8)
# 서울시 셀프, 비셀프 주유소 개수 df['is_self'].value_counts()
프린트 결과)
is_self
Y 273
N 160
Name: count, dtype: int64
서울시에는 셀프 주유소가 더 많다.
gasoline_mean = df.groupby('is_self')['gasoline'].mean() diesel_mean = df.groupby('is_self')['diesel'].mean() # 휘발유 plt.figure(figsize=(12, 8)) sns.violinplot(x="is_self", y="gasoline", data=df, palette="Set1") plt.axhline(gasoline_mean['Y'], color='red', linestyle='--', xmax=0.5, xmin=0) plt.axhline(gasoline_mean['N'], color='blue', linestyle='--', xmax=1, xmin=0.5) plt.xlabel('self', fontsize=12) plt.ylabel('gasoline price', fontsize=12) plt.grid(True) plt.show()
프린트 결과)
# 경유 plt.figure(figsize=(12, 8)) sns.violinplot(x="is_self", y="diesel", data=df, palette="Set1") plt.axhline(diesel_mean['Y'], color='red', linestyle='--', xmax=0.5, xmin=0) plt.axhline(diesel_mean['N'], color='blue', linestyle='--', xmax=1, xmin=0.5) plt.xlabel('self', fontsize=12) plt.ylabel('diesel', fontsize=12) plt.grid(True) plt.show()
프린트 결과)
비셀프 주유소의 기름 가격 분포가 더 넓지만, 전반적으로 셀프 주유소 가격이 더 저렴함
df_y = df[df['is_self'] == 'Y'] # 셀프 주유소 df_n = df[df['is_self'] == 'N'] # 비셀프 주유소 print(stats.mannwhitneyu(df_y['gasoline'], df_n['gasoline'])) print(stats.mannwhitneyu(df_y['gasoline'], df_n['gasoline']))
프린트 결과)
MannwhitneyuResult(statistic=9636.0, pvalue=2.7162145942252965e-22)
MannwhitneyuResult(statistic=9636.0, pvalue=2.7162145942252965e-22) # 왜 둘이 똑같지?
휘발유와 경유 모두 셀프 주유 가능 여부에 따른 기름값 차이가 있다. (통계적으로 유의하다)
그러나 정규화, 등분산화 등을 통해 통계적 결과에 대한 유의성을 더 높일 필요가 있다.
# 가장 비싼 휘발유 주유소 10개 df.sort_values(by="gasoline", ascending=False).head(10) # 가장 싼 휘발유 주유소 10개 df.sort_values(by="gasoline", ascending=True).head(10) # 가장 비싼 경유 주유소 10개 df.sort_values(by="diesel", ascending=False).head(10) # 가장 싼 경유 주유소 10개 df.sort_values(by="diesel", ascending=True).head(10) gu_gas_data = pd.pivot_table(data=df, index="gu", values="gasoline", columns="is_self", aggfunc=np.mean) gu_gas_data['price_diff'] = gu_gas_data['N'] - gu_gas_data['Y'] gu_ds_data = pd.pivot_table(data=df, index="gu", values="diesel", columns="is_self", aggfunc=np.mean) gu_ds_data['price_diff'] = gu_ds_data['N'] - gu_ds_data['Y'] my_map = folium.Map(location=[37.5502, 126.982], zoom_start=10.5, tiles="OpenStreetMap") folium.Choropleth( geo_data=geo_str, data=gu_gas_data, columns=[gu_gas_data.index, "price_diff"], key_on="feature.id", fill_color="Yl", ).add_to(my_map) my_map # 용산구에는 셀프 주유소가 없음 (셀프 유무에 따른 가격 비교 불가) my_map = folium.Map(location=[37.5502, 126.982], zoom_start=10.5, tiles="OpenStreetMap") folium.Choropleth( geo_data=geo_str, data=gu_ds_data, columns=[gu_ds_data.index, "price_diff"], key_on="feature.id", fill_color="PuRd" ).add_to(my_map) my_map # 용산구에는 셀프 주유소가 없음 (셀프 유무에 따른 가격 비교 불가)
서울시 소재 셀프 주유소는 비셀프 주유소에 비해 더 저렴할까?