해당 주제는 실제로 셀프 주유소가 기름값이 더 저렴한지 궁금하여 선택한 주제 입니다. 그래서 직접 주유소 홈페이지를 들어가서 크롤링 후 실제로 더 저렴한지 분석해보겠습니다.

 

1. 서울시 각 구별 주유소 데이터 크롤링

from selenium import webdriver
from selenium.webdriver.common.by import By
from bs4 import BeautifulSoup
from selenium.webdriver.support.select import Select
import time

# 대한민국 주유 가격 알아보는 사이트
url = 'https://www.opinet.co.kr/searRgSelect.do'

# 창 열기
driver = webdriver.Chrome()
driver.get(url)
driver.maximize_window()

# 서울 선택
select_Seoul = Select(driver.find_element(By.NAME, 'SIDO_NM0'))
select_Seoul.select_by_index(1)

# 구 선택
name = [] # 주유소 이름
address = [] # 주소
brand = [] # 상호명
gasoline_price = [] # 휘발유 가격
diesel_price = [] # 경유 가격
self = [] # 셀프 주유소 유무
car_wash = [] # 세차장 유무
charging_station = [] # 충전소 유무
maintenance = [] # 경정비 유무
convenience_store = [] # 편의점 유무
hour_24 = [] # 24시간 유무
gu = [] # 구
lat = [] # 위도
lng = [] # 경도

for i in range(1, 26):
    select_gu = Select(driver.find_element(By.NAME, 'SIGUNGU_NM0'))
    select_gu.select_by_index(i)
    
    time.sleep(3) # sleep 안할 시 로딩 중 클릭으로 오류 발생
    
    # 조회 누르기
    click_search = driver.find_elements(By.CLASS_NAME, 'btn_type1')
    click_search[0].click()

    time.sleep(3) # sleep 안할 시 로딩 중 클릭으로 오류 발생

    # 보통 휘발유 목록 전체
    info_list = driver.find_elements(By.ID, 'body1')
    click_gas_station = info_list[0].find_elements(By.CSS_SELECTOR,' a[href^="javascript:fn_osPop"]')
    
    # 보통 휘발유 or 경유 목록 클릭하기
    for i in range(len(click_gas_station)):
        click_gas_station[i].click()

        html = driver.page_source
        soup = BeautifulSoup(html, 'html.parser')
    
        # 가져와야할 데이터 크롤링
        name.append(soup.find('label', {'id': 'os_nm'}).text.strip())
    
        address.append(soup.find('label', {'id': 'rd_addr'}).text.strip())
        
        brand.append(soup.find('label', {'id': 'poll_div_nm'}).text.strip())
        
        gasoline_price.append(int(soup.find('label', {'id': 'b027_p'}).text.strip().replace(",", "")))
        
        diesel_price.append(int(soup.find('label', {'id': 'd047_p'}).text.strip().replace(",", "")))
        
        self_TF = bool(soup.find('img', {'alt': '셀프주유소'}))
        if self_TF:
            self.append('Y')
        else:
            self.append('N')
        
        car_wash_TF = 'off' in str(soup.find('img', {'id': 'cwsh_yn'}))
        if car_wash_TF:
            car_wash.append('N')
        else :
            car_wash.append('Y')
        
        charging_station_TF = 'off' in str(soup.find('img', {'id': 'lpg_yn'}))
        if charging_station_TF:
            charging_station.append('N')
        else :
            charging_station.append('Y')
        
        maintenance_TF = 'off' in str(soup.find('img', {'id': 'maint_yn'}))
        if maintenance_TF:
            maintenance.append('N')
        else :
            maintenance.append('Y')
        
        convenience_store_TF = 'off' in str(soup.find('img', {'id': 'cvs_yn'}))
        if convenience_store_TF:
            convenience_store.append('N')
        else :
            convenience_store.append('Y')
        
        hour_24_TF = 'off' in str(soup.find('img', {'id': 'sel24_yn'}))
        if hour_24_TF:
            hour_24.append('N')
        else :
            hour_24.append('Y')
    
    
        gu.append(address[0].split(' ')[1])
    
driver.quit()

import pandas as pd

df = pd.DataFrame({
    'name': name,
    'address': address,
    'brand': brand,
    'gasoline_price': gasoline_price,
    'diesel_price': diesel_price,
    'self': self,
    'car_wash': car_wash,
    'charging_station': charging_station,
    'maintenance': maintenance,
    'convenience_store': convenience_store,
    'hour_24': hour_24,
    'gu': gu
})

df

저는 리스트에 각각의 값들을 모두 저장하는 바람에 코드가 매우 길어졌으나 꼭 크롤링을 저처럼 하시지 않아도 됩니다.

코드 결과

코드는 매우 길지만 저희가 원하는 결과를 딱 가져온 것을 확인 할 수 있었습니다.


2. 주유소에 추가되어있는 운영 여부에 따른 상관성 분석

df.drop(columns=['name','address', 'brand','gu'], axis=1)
		.replace({'Y': 1, 'N': 0}).corr()

코드 결과

셀프 주유수 여부에서 그나마 약한 상관관계를 보입니다. 

 

하지만 저는 피어슨 상관계수(corr)를 사용하였으나, 매우 큰 실수 였었습니다. 왜냐하면 범주형 변수(Y/N이 들어있는 변수)를 피어슨 상관계수를 사용하여 분석하는데에는 한계가 있기 때문입니다.

 

해결방법!!

 

  • 포인트 바이세리얼 상관계수 계산
    연속형 변수와 이진형 범주 변수를 각각 나눠서 포인트 바이세리얼 상관계수를 계산할 수 있습니다.
  • 각 이진 변수와 연속 변수 간의 t-검정
    각 이진 변수(Y/N 변환 값)와 연속 변수 간의 차이가 유의미한지 확인하는 t-검정을 사용할 수 있습니다. 예를 들어, 기름값을 두 그룹(Y와 N)에 나눠 평균 차이가 있는지를 분석하여 상관성을 확인합니다.
  • 로지스틱 회귀 분석
    기름값의 상하위 구간을 기준으로 이진 종속 변수를 만들어, 각 변수의 기여도를 로지스틱 회귀로 확인하는 방법도 좋습니다.

해당 문제는 다음 글에서 해결해보겠습니다.


3. Self 주유소와 아닌 주유소의 휘발유 가격 대조

df.groupby('self')[['gasoline_price', 'diesel_price']].agg(['max', 'mean'])

코드 결과

 

휘발유 가격과 경유 가격에서 제일 큰 값으로 비교해본 결과 셀프 주유소가 아닌 곳의 가격이 매우 큰 것으로 확인되었습니다. 그리고 가격 평균으로 확인 해본 결과 셀프 주유소가 약 160원 정도 저렴한 것으로 확인 되었습니다.

하지만 셀프 주유소가 아닌 곳의 outlier를 확인하여 이로 인한 평균 증가인 것인지 박스 플롯을 통해 확인 해보겠습니다.


4. 박스플롯 생성 및 Outlier 제거

import seaborn as sns
import matplotlib.pyplot as plt

plt.subplot(211)
sns.boxplot(data=df, y=df['self'], x=df['gasoline_price'], hue=df['self']);

plt.subplot(212)
sns.boxplot(data=df, y=df['self'], x=df['diesel_price'], hue=df['self']);

plt.tight_layout()

코드 결과

Boxplot으로 확인 해본 결과 두 개의 그래프 모두 Outlier가 좀 보이며(특히 셀프 주유소가 아닌 곳은 말도 안되게 비쌉니다...) 셀프 주유소가 아닌 곳 들은 대체로 셀프 주유소보다 높은 가격대에 분포하는 것으로 보입니다. 

셀프 주유소에서의 outlier도 확실히 이상하긴 합니다. 평균이 1600원 정도인데 약 600원 차이도 보이기도 하구요..!!

Q1 = df[['gasoline_price', 'diesel_price']].quantile(q=0.25) # q1 값
Q3 = df[['gasoline_price', 'diesel_price']].quantile(q=0.75) # q3 값

IQR = Q3 - Q1

# 가솔린 이상치 제거 후 DataFrame
gasoline_outlier_del_df = df[(df['gasoline_price'] > Q1['gasoline_price'] - 1.5 * IQR['gasoline_price']) & (df['gasoline_price'] < Q3['gasoline_price'] + 1.5 * IQR['gasoline_price'])]

# 경유 이상치 제거 후 DataFrame
diesel_outlier_del_df = df[(df['diesel_price'] > Q1['diesel_price'] - 1.5 * IQR['diesel_price']) & (df['diesel_price'] < Q3['diesel_price'] + 1.5 * IQR['diesel_price'])]

IQR을 활용하여 이상치를 제거하였습니다.


5. 가격 비교와 박스플롯으로 다시 확인하기

이상치 제거 후 가격 비교

그래도 전 보다는 확실히  평균과 max값이 좀 더 안정된 것으로 확인 됩니다.

그리고 가솔린의 최대값은 거의 차이가 나지 않지만 평균 가격은 약 25원 정도의 차이가 있어 보입니다.

경유의 최대값 또한 거의 차이가 없으며 평균 가격은 약 50원 정도의 차이가 있습니다.

plt.subplot(211)
sns.boxplot(data=gasoline_outlier_del_df, 
            y=gasoline_outlier_del_df['self'], 
            x=gasoline_outlier_del_df['gasoline_price'], 
            hue=gasoline_outlier_del_df['self']);

plt.subplot(212)
sns.boxplot(data=diesel_outlier_del_df, 
            y=diesel_outlier_del_df['self'], 
            x=diesel_outlier_del_df['diesel_price'], 
            hue=diesel_outlier_del_df['self']);
plt.tight_layout()

코드 결과

박스 플롯 또한 확인 결과 셀프 주유소가 셀프 주유소가 아닌 곳에 비해 아주 낮은 가격대에 분포하는 것으로 보이긴 하지만 그렇게 큰 차이는 없는 것으로 보입니다.


6. 히스토그램을 통해 좀 더 확실하게 분포를 확인 해 보겠습니다.

# 셀프 주유소와 일반 주유소의 가격 분포를 히스토그램으로 확인
plt.figure(figsize=(12, 6))
# 가솔린
plt.subplot(121)
sns.histplot(data=outlier_del_df, x='gasoline_price', hue='self', kde=True, element='step')

plt.title('Gasoline Price Count Compare')
plt.xlabel('Gasoline Price')
plt.ylabel('Count')
# 디젤
plt.subplot(122)
sns.histplot(data=outlier_del_df, x='diesel_price', hue='self', kde=True, element='step')

plt.title('Diesel Price Count Compare')
plt.xlabel('Diesel Price')
plt.ylabel('Count')

plt.show()

코드 결과

확실히 셀프 주유소가 몇 십원 정도 저렴한 가격대에 많이 분포하는 것으로 확인이 됩니다.

(그럼...주유를 할 때 돈을 아끼려면 셀프 주유소 이용이 좋아 보이기도 합니다... 제 초보적인 분석결과이니 너무 믿으시면 안됩니다..)


결론

  • 이상치 제거 하지 않고 박스플롯과 최댓값과 평균값을 비교하였을 때 약 160원 정도 셀프 주유소가 훨씬 더 저렴한 것을 확인 할 수 있었습니다.
  • 그래도 이상치에 의해 평균값이 올라가 잘못된 측정을 한 것일 수도 있다 판단하여 이상치를 제거하는 작업을 하고 다시 측정해보았습니다.
  • 이상치를 제거 후 다시 최댓값과 평균값을 확인 해보았을 때 커봐야 20원 ~ 40원 정도의 차이로 셀프 주유소가 매우 약간 저렴한 것을 볼 수 있었습니다.
  • 그리고 더 저렴한 주유소가 더 많은지에 대한 분포를 확인하기 위해 히스토그램을 사용하여 시각화한 결과 확실히 경유와 휘발유의 가격이 좀 더 싼 주유소가 많이 분포 하는 것을 확인 할 수 있었습니다.

다음 글에서는 변수 간 상관성을 제대로 분석하고 운영 여부에 따른 값의 변동이 있는지도 한 번 확인 해보겠습니다.

이 글은 제로베이스 데이터 분석 취업 스쿨의 강의 자료 일부를 발췌하여 작성되었습니다.

이상입니다.

728x90
hmm06