07_Data_Analysis

21_청소기 가성비 데이터 수집&분석

chuuvelop 2025. 3. 25. 23:12
728x90

분석 목표

  • 여러 제조사별 무선청소기의 가격과 성능에 대한 데이터를 수집하고 분석하여 각각의 상황에 맞는 제품군 파악
from selenium import webdriver
from seleniuhttp://m.webdriver.common.by import By
from bs4 import BeautifulSoup
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import time

 

# Windows용 한글 폰트 오류 해결
from matplotlib import font_manager, rc
font_path = "C:/Windows/Fonts/malgun.ttf"
font_name = font_manager.FontProperties(fname = font_path).get_name()
rc("font", family = font_name)

 

 

데이터 수집

url = "https://prod.danawa.com/list/?cate=102207&shortcutKeyword=%EB%AC%B4%EC%84%A0%EC%B2%AD%EC%86%8C%EA%B8%B0"

driver = webdriver.Chrome()
driver.get(url)
driver.implicitly_wait(3)
soup = BeautifulSoup(driver.page_source, "lxml")
prod_items = soup.select("div.main_prodlist li.prod_item")

 

len(prod_items)
30

 

# 상품명
prod_items[0].select_one("p.prod_name > a").string.strip()
'아이닉 i50 아이타워'

 

# 스펙 목록
prod_items[0].select_one("div.spec-box--full > div.spec_list").text.strip()
'핸디스틱청소기/무선/흡입형/흡입력: 520W/흡입력: 45,000Pa/스테이션 기능: 먼지비움, 충전, 액서서리수납, 스탠드거치/브러쉬: 전방향바닥/침구/솔형/틈새/먼지봉투: 1.5L/[배터리] 사용시간: 1시간05분(최대)/분리형(1개)/[청소] LED라이트/UV살균/BLDC모터/헤파필터/무게: 2.7kg/크기(가로x세로x깊이): 250x1040x260mm'

 

 

# 가격
prod_items[5].select_one("p.price_sect strong").string.replace(",", "").strip()
'1170000'

 

 

prod_data = {"상품명" : [],
             "스펙목록" : [],
             "가격" : []}

prod_items = soup.select("div.main_prodlist li.prod_item")

for item in prod_items:
    # 상품명
    title = item.select_one("p.prod_name > a").string.strip()
    # 스펙 목록
    spec_list = item.select_one("div.spec-box--full > div.spec_list").text.strip()
    # 가격
    price = item.select_one("p.price_sect strong").string.replace(",", "").strip()

    prod_data["상품명"].append(title)
    prod_data["스펙목록"].append(spec_list)
    prod_data["가격"].append(price)

 

 

len(prod_data["상품명"]), len(prod_data["스펙목록"]), len(prod_data["가격"])
(30, 30, 30)

 

prod_data["상품명"][:5]
['아이닉 i50 아이타워',
 'LG전자 오브제컬렉션 코드제로 A9S AU927CWD',
 'LG전자 코드제로 A9 Air AS9000WR',
 'LG전자 오브제컬렉션 코드제로 A9S AX9604WE',
 'LG전자 오브제컬렉션 코드제로 A9S AX958BWE']

 

prod_data["스펙목록"][:5]
['핸디스틱청소기/무선/흡입형/흡입력: 520W/흡입력: 45,000Pa/스테이션 기능: 먼지비움, 충전, 액서서리수납, 스탠드거치/브러쉬: 전방향바닥/침구/솔형/틈새/먼지봉투: 1.5L/[배터리] 사용시간: 1시간05분(최대)/분리형(1개)/[청소] LED라이트/UV살균/BLDC모터/헤파필터/무게: 2.7kg/크기(가로x세로x깊이): 250x1040x260mm',
 '핸디스틱청소기/무선/흡입+물걸레(동시)/흡입력: 260W/스테이션 기능: 먼지비움, 충전, UVC LED, 액서서리수납, 스탠드거치/먼지비움시간: 30초/브러쉬: 바닥/물걸레: 일반/솔형/틈새/먼지봉투: 2.5L/[배터리] 사용시간: 1시간(최대)/분리형(1개)/리튬이온/[청소] 싸이클론흡입/자동물공급/스마트인버터모터/워셔블헤파필터/무게: 2.5kg/크기(가로x세로x깊이): 250x1120x260mm',
 '핸디스틱청소기/무선/흡입형/흡입력: 150W/스테이션 기능: 충전, 스탠드거치, 액서서리&배터리거치/브러쉬: 바닥/솔형/틈새/틈새(내장형)/[배터리] 사용시간: 40분(최대)/분리형(1개)/리튬이온/[청소] 싸이클론흡입/스마트인버터모터/무게: 1.97kg/크기(가로x세로x깊이): 251x1026x242mm',
 '핸디스틱청소기/무선/흡입형/흡입력: 250W/스테이션 기능: 먼지비움, 충전, UVC LED, 액서서리수납, 스탠드거치/먼지비움시간: 30초/브러쉬: 와이드바닥/물걸레: 별매/솔형/틈새/먼지봉투: 2.5L/[배터리] 사용시간: 30분(최대)/분리형(1개)/리튬이온/[청소] LED라이트/싸이클론흡입/스마트인버터모터/워셔블헤파필터/무게: 2.47kg/크기(가로x세로x깊이): 300x1120x245mm',
 '핸디스틱청소기/무선/흡입+물걸레(동시)/흡입력: 280W/스테이션 기능: 먼지비움, 충전, 액서서리수납, 스탠드거치/먼지비움시간: 30초/브러쉬: 와이드바닥/물걸레: 스팀, 고온, 일반/침구/솔형/틈새/먼지봉투: 2.5L/[배터리] 사용시간: 30분(최대)/분리형(2개)/리튬이온/[청소] LED라이트/싸이클론흡입/오토스탑앤고/자동물공급/스마트인버터모터/워셔블헤파필터/무게: 2.49kg/크기(가로x세로x깊이): 300x1100x245mm']

 

 

prod_data["가격"][:5]
['296980', '702340', '363080', '689370', '978500']

 

 

# 10페이지까지 반복
prod_data = {"상품명" : [],
             "스펙목록" : [],
             "가격" : []}

driver = webdriver.Chrome()
driver.get(url)

for i in range(10):
    time.sleep(2)
    soup = BeautifulSoup(driver.page_source, "lxml")
    
    prod_items = soup.select("div.main_prodlist li.prod_item")
    
    for item in prod_items:
        # 상품명
        title = item.select_one("p.prod_name > a").string.strip()
        # 스펙 목록
        spec_list = item.select_one("div.spec-box--full > div.spec_list").text.strip()
        # 가격
        price = item.select_one("p.price_sect strong").string.replace(",", "").strip()
    
        prod_data["상품명"].append(title)
        prod_data["스펙목록"].append(spec_list)
        prod_data["가격"].append(price)

    if i != 9:
        driver.find_element(By.CSS_SELECTOR, "a.now_on + a").click()

 

 

데이터 전처리

데이터 확인

df = pd.DataFrame(prod_data)
df.head()

 

df.shape
(300, 3)

 

df.to_csv("./data/danawa.csv", index = False)
# 250324 저장한 데이터를 불러옴
df = pd.read_csv("./data/danawa.csv")

 

df.dtypes
상품명     object
스펙목록    object
가격      object
dtype: object

 

# 스펙목록 전처리
def clean_text(text):
    text = text.replace("\n", "")
    text = text.replace("\t", "")
    text = text.replace(" ", "")
    return text

 

df["스펙목록"] = df["스펙목록"].map(clean_text)

 

df.head()

 

 

df["가격"].unique()
array(['296980', '702340', '363080', '689370', '978500', '1170000',
       '145082', '635200', '462570', '639610', '205560', '1293560',
       '696000', '772430', '1057890', '361890', '189770', '583810',
       '494900', '649800', '724000', '728880', '423660', '328830',
       '299000', '1109630', '805000', '179000', '595830', '797990',
       '820800', '491110', '424480', '190600', '565890', '67790', '87900',
       '1060750', '449100', '459620', '219100', '827690', '744920',
       '69000', '200630', '109110', '459860', '800990', '1259050',
       '39590', '364900', '39900', '1151720', '423200', '515120',
       '324570', '303340', '1009780', '29240', '99000', '832950',
       '198000', '960300', '1023110', '42780', '61020', '442170',
       '111500', '453890', '904150', '148570', '743870', '236900',
       '148000', '81530', '205730', '128000', '315280', '803000',
       '159000', '393970', '349000', '145000', '230000', '265050',
       '634700', '158000', '580310', '219000', '1104760', '759960',
       '119000', '93060', '186000', '258900', '44630', '106000', '117000',
       '219450', '94000', '104890', '209470', '197690', '199510',
       '259000', '554600', '269700', '199000', '396880', '104900',
       '523570', '295640', '814000', '52890', '178000', '118000',
       '348750', '512250', '691780', '292080', '907960', '154850',
       '799590', '129000', '173230', '1150990', '716680', '138990',
       '1177000', '42750', '426000', '40000', '579000', '1280450',
       '80060', '695410', '425180', '750000', '502200', '689700',
       '942410', '98440', '24090', '194750', '444870', '1119980',
       '720000', '928020', '500450', '189000', '510000', '57000',
       '366500', '281680', '339000', '249000', '189800', '98000',
       '636760', '168990', '176000', '402570', '1136920', '549000',
       '1168990', '192450', '95400', '418990', '70400', '112480',
       '369550', '274500', '1197990', '557650', '46800', '49770',
       '290000', '117870', '650000', '949980', '68940', '187060',
       '789990', '149000', '66210', '63900', '22700', '82700', '999000',
       '319800', '469000', '276780', '462060', '160600', '857000',
       '629000', '47800', '155000', '379620', '30690', '289000', '49800',
       '1320470', '102490', '664280', '467350', '244000', '519000',
       '269000', '154750', '137730', '1072950', '172050', '288840',
       '1000000', '374120', '1727460', '94050', '116100', '175750',
       '229800', '56110', '711760', '50000', '46910', '108990', '178990',
       '735420', '640000', '297570', '357520', '185730', '279500',
       '754120', '124000', '1061620', '44900', '473960', '473590',
       '1149220', '9500', '142080', '329000', '93010', '42900', '649000',
       '1115090', '957100', '50940', '23730', '52000', '577250', '34670',
       '775350', '가격비교예정', '84600', '367000', '724500', '1120000',
       '57690', '456690', '32900', '74900', '35340', '1569990', '104250',
       '948000', '183370', '215720', '294190', '658450', '818670',
       '618990', '438870', '243040', '135900', '598990', '198990',
       '59800', '37990'], dtype=object)

 

 

df = df[df["가격"] != "가격비교예정"]
# 가격 컬럼을 정수형으로 변환
df["가격"] = df["가격"].astype(int)
df.dtypes
상품명     object
스펙목록    object
가격       int32
dtype: object

 

 

회사명, 모델명 정리

  • 상품명 데이터는 회사명 + 모델명의 형식임
    • 브랜드와 모델명으로 분리
df.iloc[0, 0].split(" ", 1) # 첫 번째 공백에 대해서만 구분하도록
['아이닉', 'i50 아이타워']

 

titles = df["상품명"].str.split(n = 1)
df["회사명"] = titles.str[0]
df["제품"] = titles.str[1]

 

df.head()

 

 

df = df.drop("상품명", axis = 1)
df.head()

 

 

스펙 목록 살펴보기

df.iloc[0, 0]
'핸디스틱청소기/무선/흡입형/흡입력:520W/흡입력:45,000Pa/스테이션기능:먼지비움,충전,액서서리수납,스탠드거치/브러쉬:전방향바닥/침구/솔형/틈새/먼지봉투:1.5L/[배터리]사용시간:1시간05분(최대)/분리형(1개)/[청소]LED라이트/UV살균/BLDC모터/헤파필터/무게:2.7kg/크기(가로x세로x깊이):250x1040x260mm
df.iloc[1, 0].split("/")
['핸디스틱청소기',
 '무선',
 '흡입+물걸레(동시)',
 '흡입력:260W',
 '스테이션기능:먼지비움,충전,UVCLED,액서서리수납,스탠드거치',
 '먼지비움시간:30초',
 '브러쉬:바닥',
 '물걸레:일반',
 '솔형',
 '틈새',
 '먼지봉투:2.5L',
 '[배터리]사용시간:1시간(최대)',
 '분리형(1개)',
 '리튬이온',
 '[청소]싸이클론흡입',
 '자동물공급',
 '스마트인버터모터',
 '워셔블헤파필터',
 '무게:2.5kg',
 '크기(가로x세로x깊이):250x1120x260mm']
 
  • 카테고리 : 스펙 리스트의 첫 번째 항목에 위치
  • 사용시간 : "사용시간: 00시간" 과 같이 사용시간 이라는 문구가 명시되어 있음
  • 흡입력 : "흡입력:000W" 와 같이 흡입력 이라는 문구가 명시되어 있음\

 

# test
spec_list = df.iloc[0, 0].split("/")
category = spec_list[0]
category
'핸디스틱청소기'

 

for spec in spec_list:
    if "사용시간" in spec:
        use_time_spec = spec
        use_time_spec = use_time_spec.split(":")[1]

    elif "흡입력" in spec:
        suction_spec = spec
        suction_spec = suction_spec.split(":")[1]

print(use_time_spec)
print(suction_spec)
1시간05분(최대)
45,000Pa

 

 

# 반복문
category_list = []
use_time_list = []
suction_list = []

for spec_data in df["스펙목록"]:
    # / 기준으로 분리
    spec_list = spec_data.split("/")

    # 카테고리 추출
    category = spec_list[0]
    category_list.append(category)

    # 사용시간, 흡입력 추출
    use_time_spec = None
    suction_spec = None

    for spec in spec_list:
        if "사용시간" in spec:
            use_time_spec = spec.split(":")[1]

        elif "흡입력" in spec:
            suction_spec = spec.split(":")[1]

    use_time_list.append(use_time_spec)
    suction_list.append(suction_spec)

 

category_list[:5]
['핸디스틱청소기', '핸디스틱청소기', '핸디스틱청소기', '핸디스틱청소기', '핸디스틱청소기']

 

use_time_list[:5]
['1시간05분(최대)', '1시간(최대)', '40분(최대)', '30분(최대)', '30분(최대)']

 

 

suction_list[:5]
['45,000Pa', '260W', '150W', '250W', '280W']

 

  • 숫자형 데이터가 아니고 단위 통일이 안 되어 있어서 통일시켜줘야함

 

 

사용시간 단위 통일

  • 분 단위로 통일
    • "시간" 단어가 있으면
      • "시간" 앞의 숫자를 추출한 뒤 60을 곱해 사용시간에 더함
      • "시간" 뒤의 글자에서 "분" 글자 앞의 숫자를 추출해서 사용시간에 더함
    • "시간" 단어가 없으면
      • "분" 글자 앞의 숫자를 추출해서 사용시간에 더함
    • 위 방식으로 계산되지 않을 경우 예외로 처리
def convert_time_minute(time):
    try:
        if "시간" in time:
            hour = time.split("시간")[0]
            if "분" in time:
                minute = time.split("시간")[-1].split("분")[0]
            
            else:
                minute = 0
        else:
            hour = 0
            minute = time.split("분")[0]
            if "~" in minute:
                minute = minute.split("~")[1]

        return int(hour) * 60 + int(minute)


    except:
        return None

 

# 테스트1
convert_time_minute(use_time_list[0])
65

 

# 테스트2
for i in use_time_list[:10]:
    print(i)
    print(convert_time_minute(i))
1시간05분(최대)
65
1시간(최대)
60
40분(최대)
40
30분(최대)
30
30분(최대)
30
30분(최대)
30
50분(최대)
50
1시간(최대)
60
1시간(최대)
60
1시간(최대)
60
 

 

new_use_time_list = []

for i in use_time_list:
    new_use_time_list.append(convert_time_minute(i))

 

df["사용시간"] = new_use_time_list

 

 

df.head()

 

 

 

흡입력 단위 통일

  • 사용하는 단위는 AW, W, PA
  • 단위간 환산은 대략(1W = 1AW = 100PA)
  • 위 전환식으로 AW기준으로 단위를 통일
  • 전처리 과정
    • 모든 알파벳 문자를 대문자로 수정
    • 흡입력에 "AW"나 "W" 글자가 있다면
      • 흡입력에서 "A"와 "W"를 삭제
      • ","를 삭제한 뒤 숫자형 데이터로 변환
    • 흡입력에 "PA"글자가 있다면
      • 흡입력에서 "PA"글자를 삭제
      • "," 를 삭제한 뒤 정수로 변환하고 100으로 나눔
    • 흡입력 값이 비어 있거나 단위 변환시 에러가 날 경우 예외로 처리

 

def get_suction(value):
    try:
        value = value.upper()
        if "W" in value:
            result = value.replace("A", "").replace("W", "")
            result = int(result.replace(",", ""))

        elif "PA" in value:
            result = value.replace("PA", "")
            result = int(result.replace(",", "")) / 100

        else:
            result = None

        return result

    except:
        return None

 

for i in suction_list[:10]:
    print(get_suction(i))
450.0
260
150
250
280
250
None
260
130
220

 

new_suction_list = []

for power in suction_list:
    new_suction_list.append(get_suction(power))
df["흡입력"] = new_suction_list
df.head()

 

df["카테고리"] = category_list
df.head()

 

 

pd_data = df[["카테고리", "회사명", "제품", "가격", "사용시간", "흡입력"]]
pd_data.head()

 

 

pd_data["카테고리"].value_counts()
카테고리
핸디스틱청소기    274
핸디청소기       19
스틱청소기        6
Name: count, dtype: int64

 

  • 핸디스틱 청소기에 대해서만 분석
pd_data_final = pd_data[pd_data["카테고리"] == "핸디스틱청소기"]
pd_data_final.head()

 

 

pd_data_final.shape
(274, 6)

 

pd_data_final.to_excel("./data/danawa_data_final.xlsx", index = False)

 

 

데이터 확인

pd_data_final.sort_values("흡입력", ascending = False).head()

 

 

pd_data_final.sort_values("사용시간", ascending = False).head()

 

 

pd_data_final.sort_values(["사용시간", "흡입력"], ascending = False).head()

 

 

pd_data_final.sort_values(["사용시간", "흡입력"], ascending = False).head()

 

 

 

가성비 좋은 제품 찾아보기

  • 가성비가 좋다: 가격 대비 성능이 우수하다
    • 각 모델을 가격, 흡입력, 사용시간의 평균값과 비교
price_mean_value = pd_data_final["가격"].mean()
suction_mean_value = pd_data_final["흡입력"].mean()
use_time_mean_value = pd_data_final["사용시간"].mean()

print(price_mean_value)
print(suction_mean_value)
print(use_time_mean_value)
432645.8832116788
216.53669724770643
43.43181818181818

 

  • 가성비 좋은 제품: 가격 43만원 이하, 흡입력 217이상, 사용시간 44분 이상
condition_data = pd_data_final[
    (pd_data_final["가격"] <= price_mean_value) &
    (pd_data_final["흡입력"] >= suction_mean_value) &
    (pd_data_final["사용시간"] >= use_time_mean_value)
]
condition_data

...

 

 

# 가격이 낮은 순으로 정렬
condition_data.sort_values("가격", ascending = True)

...

 

condition_data["회사명"].value_counts()
회사명
아이닉       6
DIBEA     6
드리미       2
비브르       2
캐치웰       2
신일전자      1
에어메이드     1
비즈포비즈     1
쿠쿠전자      1
샤오미       1
아토케어      1
차이슨       1
퍼피유       1
에버튼하우스    1
JONR      1
Name: count, dtype: int64
  • DIBEA, 아이닉 제품들이 가성비가 좋음

 

데이터 시각화

pd_data_final.isna().sum()
카테고리     0
회사명      0
제품       0
가격       0
사용시간    10
흡입력     56
dtype: int64

 

# 결측치 있는 행 제거
chart_data = pd_data_final.dropna(axis = 0)
chart_data.shape
(209, 6)

 

 

# 흡입력, 사용시간의 최댓값/평균값 정리
suction_max_value = chart_data["흡입력"].max()
suction_mean_value = chart_data["흡입력"].mean()
suction_min_value = chart_data["흡입력"].min()

use_time_max_value = chart_data["사용시간"].max()
use_time_mean_value = chart_data["사용시간"].mean()
use_time_min_value = chart_data["사용시간"].min()
print(suction_max_value)
print(suction_mean_value)
print(suction_min_value)

print(use_time_max_value)
print(use_time_mean_value)
print(use_time_min_value)
600.0
215.28708133971293
22.0
100.0
44.51196172248804
9.0
plt.figure(figsize = (20, 10))
plt.title("무선 핸디스틱청소기 차트")

sns.scatterplot(x = "흡입력", y = "사용시간", size = "가격", hue = chart_data["회사명"],
                data = chart_data, sizes = (10, 1000), legend = False)
plt.plot([suction_min_value, suction_max_value],
         [use_time_mean_value, use_time_mean_value],
         "r--",
         lw = 1)
plt.plot([suction_mean_value, suction_mean_value],
         [use_time_min_value, use_time_max_value],
         "r--",
         lw = 1)
plt.show()

 

  • 사용시간이 길고 흡입력이 큰 제품이 가격이 높아지는 경향이 있음

 

인기제품 10개 모델 시각화

chart_data_selected = chart_data[:10]
chart_data_selected

 

suction_max_value = chart_data_selected["흡입력"].max()
suction_mean_value = chart_data_selected["흡입력"].mean()
suction_min_value = chart_data_selected["흡입력"].min()

use_time_max_value = chart_data_selected["사용시간"].max()
use_time_mean_value = chart_data_selected["사용시간"].mean()
use_time_min_value = chart_data_selected["사용시간"].min()

 

plt.figure(figsize = (20, 10))
plt.title("무선 핸디스틱청소기 Top10 차트")

sns.scatterplot(x = "흡입력", y = "사용시간", size = "가격", hue = chart_data_selected["회사명"],
                data = chart_data_selected, sizes = (10, 1000), legend = False)
                # 회사 종류가 적으므로 legend = True로 변경
plt.plot([suction_min_value, suction_max_value],
         [use_time_mean_value, use_time_mean_value],
         "r--",
         lw = 1)
plt.plot([suction_mean_value, suction_mean_value],
         [use_time_min_value, use_time_max_value],
         "r--",
         lw = 1)

# 각 점에 텍스트를 찍어서 표현
for idx, row in chart_data_selected.iterrows():
    x = row["흡입력"]
    y = row["사용시간"]
    s = row["제품"].split(" ")[0]
    plt.text(x, y, s, size = 20)
    
plt.show()

728x90