뽐뿌 특가 데이터 전처리 하기

2024. 11. 24. 13:11·Data Science/Data Engineering
728x90
반응형
특가 정보에 관심이 많은 사람으로써 특가 데이터 분석을 위해 뽐뿌의 특가 게시판을 크롤링 하여 특가 데이터를 확보하였고, 그 데이터를 전처리하였다. 특가 데이터 분석은 1)데이터 확보(크롤링) 2)데이터 전처리 3)특가 데이터 분석 4)카테고리 예측 모델링순으로 진행된다.

1. 개요

개요

소제목

데이터 분석을 시작하기 전에, 정확한 데이터 분석을 위해 전처리 과정이 필요하다. 데이터 분석가 업무의 80%는 데이터 전처리라는 우스갯소리를 할 정도로 굉장히 많은 시간이 들어가고 많은 고민을 하는것이 데이터 전처리 과정이다. 이번 뽐뿌 특가 데이터 분석에도 데이터 전처리는 빠질수 없는 과정으로 분석을 진행하기 용이하기 데이터 전처리를 진행했다.

데이터 전처리 과정

데이터 전처리는 아래의 3개 과정을 통해 진행되었다.

  1. 특성 추출: 게시물 제목에서 판매채널, 제품 가격, 배송비 정보를 추출했다.
  2. 데이터 정제: 추출한 특성에서 결측치, 이상치, 정합성 확인, 통합 등을 처리하여 데이터의 일관성과 정확성을 높였다
  3. 데이터 변환: 문자열을 숫자로 변환하는 등의 필요한 형태로 데이터를 변환했다.

결론

데이터 전처리를 통해 특가 게시물의 핵심 정보인 판매 채널, 제품 금액, 배송비, 그리고 키워드를 추출하였다. 이러한 과정에서 판매 채널의 통합, 금액 정보의 정제 및 키워드의 최적화 작업으로 데이터 품질을 향상시켰다. 이렇게 향상된 데이터는 특가를 찾는 사용자들의 관심도와 반응을 파악하는 데 큰 도움을 제공할 것이다. 따라서, 전처리된 데이터는 특가 정보의 특성과 트렌드를 더욱 명확하게 보여주며, 사용자들이 더 현명한 소비 결정을 내릴 수 있도록 도와줄 것이다.

데이터 전처리 코드

Package and Data load

데이터 전처리 전에 패키지를 임포트하고 데이터를 로드하여 데이터를 확인한다. 또한, 컬럼의 정의는 아래와 같다.

import pandas as pd
import numpy as np
import re

from tqdm import tqdm
from kiwipiepy import Kiwi
from datetime import datetime
df = pd.read_csv('./datas/2023-06-30 22:27:20.666568_117980개.csv')
df.head(2)
  item_no writer title end comment date recommend opposite view category URL pop hot
0 470673 Ko**** [cj온스타일] 아이더 반팔 기능티 2장 (21,600원/무료) True 8 23.06.29 20:39:22 1 1 7125 [의류/잡화] https://www.ppomppu.co.kr/zboard/view.php?id=p... False False
1 470672 아**** [G마켓] PS5 디스크 에디션 갓오워 라그나로크 에디션(1218A) (606,97... True 15 23.06.29 20:03:40 0 0 9811 [가전/가구] https://www.ppomppu.co.kr/zboard/view.php?id=p... False False
Column 설명
item_no 게시물 번호
Author 작성자
Title 게시물 제목
end 특가 종료 여부
Comments 댓글 수
Date 게시 날짜
recommend 추천수
opposite 반대수
view 조회수
Category 특가 제품이 속한 카테고리
URL URL
pop 인기 게시물 여부
hot 핫 게시물 여부

데이터 요약 정보

데이터 전처리의 시작으로 크롤링된 데이터의 요약자료를 보았다. 눈에 뜨는것은 댓글이 1401개 있는 게시물인데, 확인해보니 P11 가성비 태블릿이 역대급 특가였으나, 실제로는 가격 오류로 인한것이였으며, 주문 제품은 모두 취소처리된 게시물이다.

df.describe()
  item_no comment recommend opposite view
count 117980.000000 117980.000000 117980.000000 117980.000000 117980.000000
mean 387483.181395 30.878047 6.636947 0.207391 14840.913062
std 48189.476587 32.625210 13.671075 1.476107 10264.550307
min 305204.000000 0.000000 0.000000 0.000000 731.000000
25% 345619.750000 11.000000 0.000000 0.000000 7676.000000
50% 387378.500000 21.000000 2.000000 0.000000 12169.000000
75% 428465.250000 39.000000 7.000000 0.000000 19035.000000
max 470673.000000 1401.000000 582.000000 141.000000 427882.000000
df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 117980 entries, 0 to 117979
Data columns (total 13 columns):
 #   Column     Non-Null Count   Dtype 
---  ------     --------------   ----- 
 0   item_no    117980 non-null  int64 
 1   writer     117980 non-null  object
 2   title      117980 non-null  object
 3   end        117980 non-null  bool  
 4   comment    117980 non-null  int64 
 5   date       117980 non-null  object
 6   recommend  117980 non-null  int64 
 7   opposite   117980 non-null  int64 
 8   view       117980 non-null  int64 
 9   category   117980 non-null  object
 10  URL        117980 non-null  object
 11  pop        117980 non-null  bool  
 12  hot        117980 non-null  bool  
dtypes: bool(3), int64(5), object(5)
memory usage: 9.3+ MB

 

제목에서 판매 채널, 가격 정보 가져오기

뽐뿌게시판은 제목 맨앞에 판매 채널, 맨 뒤에 가격을 적는것을 규칙으로 하고 있으며, 이번에는 해당 규칙을 활용해 판매 채널과 가격을 제목에서 추출했다. 다만, 게시물 작성 규칙을 지키 않아도 게시글은 작성이 되므로 예외 처리된 데이터도 있을것으로 판단되며, 예외 처리되는 데이터를 줄여야 한다. 전체 특가 데이터는 117,980개로 Null값은 없는것으로 보인다. 또한 int형태와 object 형태로만 되어있어서 데이터를 정리할 필요가 있어보인다.

def extract_sales_channel_and_price(title):
    """ 특가 게시물 제목에서 가격, 판매채널 추출
    Args:
        title - 특가 게시물 제목
    Returns:
        str : 아래의 데이터를 가진 str형식 return
            - sales_channel : 특가 판매 채널 (e.g 지마켓)
            - price : 제품/배송비 가격
    """
    # 문자열에서 [...] 혹은 (...) 형태의 구성을 찾아 추출
    pattern = r"\[([^\]]+)\]|\(([^\)]+)\)"

    # 가격, 판매채널 추출
    matches = re.findall(pattern, title)

    # 판매채널
    sales_channel = matches[0][0] or matches[0][1] if matches else "unknown"
    sales_channel = sales_channel.strip()

    # 가격
    price = matches[-1][0] or matches[-1][1] if matches else "unknown"  # Return the last match
    price = price.strip()

    return sales_channel, price

# 제목에서 판매채널과 가격 추출
df['sales_channel'], df['price'] = zip(*df['title'].map(extract_sales_channel_and_price))
df[['title', 'sales_channel', 'price']].head(2)
  title sales_channel price
0 [cj온스타일] 아이더 반팔 기능티 2장 (21,600원/무료) cj온스타일 21,600원/무료
1 [G마켓] PS5 디스크 에디션 갓오워 라그나로크 에디션(1218A) (606,97... G마켓 606,970/무료

제폼 / 배송비 가격 정보에서 제품 가격과 배송비 정보를 분리

price 컬럼을 보면 제품 가격과 배송비가 같이 적혀있으므로, 이를 다시 분리해준다.

def split_price(price):
    """ 제품/배송비 가격에서 제품 가격과 배송비 분리
    Args:
        price - 제품/배송비 가격
    Returns:
        str : 아래의 데이터를 가진 str형식 return
            - product_price : 특가 제품 가격
            - shipping_cost : 배송비
    """
    # price에 배송비가 없는 경우도 있으므로, 있으면 제품가격과 배송비, 없으면 제품가격과 unknown으로 리턴
    if "/" in price:
        product_price, shipping_cost = price.split("/", 1)  # Split into at most 2 parts
    else:
        product_price = price
        shipping_cost = "unknown"
    return product_price, shipping_cost

# 가격에서 제품 가격과 배송비 구별
df['product_price'], df['shipping_cost'] = zip(*df['price'].map(split_price))
df[['title', 'sales_channel', 'price', 'product_price', 'shipping_cost']].head(2)
  title sales_channel price product_price shipping_cost
0 [cj온스타일] 아이더 반팔 기능티 2장 (21,600원/무료) cj온스타일 21,600원/무료 21,600원 무료
1 [G마켓] PS5 디스크 에디션 갓오워 라그나로크 에디션(1218A) (606,97... G마켓 606,970/무료 606,970 무료

판매 채널 통합

판매 채널의 갯수는 5,992개이지만 사람이 직접 적는것으로 같은 판매 채널이여도 약어로 적거나 별칭 등 다르게 적을 수 있어서 동일한 채널이라면 하나의 판매채널로 통합한다. 정리하여 5,992개에서 3,751개로 38% 가량 통합하였다.

df["sales_channel"].value_counts()
G마켓        14920
11번가       12163
옥션         11152
위메프         9173
티몬          8857
           ...  
롯데온앱           1
쎄제이            1
파파존스           1
옥션스마일클럽        1
NS쇼핑몰          1
Name: sales_channel, Length: 5992, dtype: int64
def channel_merge(df, channel, change_channel):
    """ 판매 채널 명
    Args:
        df - 특가 게시물 Dataframe
        channel - 변경 전 채널 이름 
        change_channel - 변경될 채널 이름

    Returns:
        Dataframe : 통합 채널명으로 변경된 DataFrame
    """

    # 통합될 채널 이름 찾기
    temp = df[df["sales_channel"].str.contains(channel, case=False)]
    # 변경할 index 저장
    change_value_idx = temp.index
    # index를 기준으로 변경 될 채널이름으로 변경
    df.loc[change_value_idx, "sales_channel"] = change_channel
    return df

channel_dict = {"네이버":["네이버", "스마트스토어", "스토어팜", "원쁠딜"],
                "11번가":["11번가", "11st", "11마존", "쇼킹딜"],
                "신세계":["신세계", "SSG"],
                "하이마트":["하이마트"],
                "롯데":["롯데", "칠성몰"],
                "카카오":["카카오", "톡딜", "카톡", "톡스토어"],
                "티몬":["티몬", "tmon", "티켓몬스터"],
                "CJ":["CJ"],
                "그립":["그립", "grip"],
                "우체국":["우체국"],
                "쿠팡":["쿠팡", "ㅋㅍ"],
                "보고":["보고","vogo"],
                "인터파크":["인터파크"],
                "AK몰":["ak"],
                "큐텐":["큐텐", "Qo", "큐10", "Q10"],
                "Quube":["Quube"],
                "GS":["gs", "나만의 냉장고"],
                "지마켓/옥션":["지마켓", "옥션","지/옥", "쥐마켓", "g마켓", "g9", "지9", "지구", "gmarket", "지옥", "옥베이"],
                "SK":["sk"],
                "아이허브":["ih"],
                "KT":["kt"],
                "Hmall":["hm", "H패", "현대몰", "h몰"],
                "홈플러스":["홈플"],
                "NS홈쇼핑":["ns"],
                "이마트":["이마트몰"],
                "메가마트":["메가마트"],
                "오늘의집":["오늘의"],
                "전자랜드":["전자랜드"],
                "나이키":["나이키"],
                "예스24":["yes"],
                "코스트코":["코스트코"],
                "Steam":["Steam", "스팀", "Indiegala"],
                "아디다스":["아디다스"],
                "홈앤쇼핑":["홈&"],
                "삼성":["삼성"],
                "신한":["신한"],
                "크록스":["크록스"],
                "국민":["국민", "국카"],
                "다이슨":["다이슨"],
                "리복":["리복"],
                "LF스퀘어몰":["LF"],
                "K쇼핑":["K쇼핑"],
                "CGV":["CGV"],
                "배달의민족":["배민"],
                "동원몰":["동원"],
                "탑텐":["탑텐"],
                "위메프":["위메프"],
                "unknown":["종료", "끌어올림", "끌올", "무배", "다양", "공홈"]
               }

for change_channel, channels in channel_dict.items():
    for channel in channels:
        df = channel_merge(df, channel, change_channel)

df["sales_channel"].value_counts()
지마켓/옥션         35548
11번가           14834
위메프             9247
티몬              9009
네이버             6828
               ...  
110만원대 /무료         1
올렛츠                1
Folderstyle        1
모요/스마텔             1
리브메이트앱             1
Name: sales_channel, Length: 3751, dtype: int64

 

제품 가격의 자료형을 Str에서 Float형으로 변환

가격 데이터 분석에 용이하기 위해 unknown을 넘파이를 이용하여 NaN으로 변환하고, 그 외 자료는 원글씨를 제외하고 숫자만 남긴다.

def convert_price_to_int(price):
    """ 제품 가격 자료형 변환
    Args:
        price - : Str 형식의 제품 가격
    Returns:
        Nan : unknown일때
        int : Int형 가격
    """

    # unknown은 NaN값
    if price == "unknown":
        return np.NaN
    # 그 외 가격은 "원", "," "."을 삭제한 숫자형
    else:
        cleaned_price = price.replace("원", "").replace(",", "").replace(".", "").strip()
        if cleaned_price.isdigit():
            return int(cleaned_price)
        else:
            return np.NaN
df['product_price'] = df['product_price'].map(convert_price_to_int)
df[['title', 'sales_channel', 'price', 'product_price', 'shipping_cost']].head()
  title sales_channel price product_price shipping_cost
0 [cj온스타일] 아이더 반팔 기능티 2장 (21,600원/무료) CJ 21,600원/무료 21,600.0 무료
1 [G마켓] PS5 디스크 에디션 갓오워 라그나로크 에디션(1218A) 지마켓/옥션 606,970/무료 606,970.0 무료
2 [네이버] 국내산 1등급 소고기 등심 200G (9,900원/4000원) 네이버 9,900원/4000원 9,900.0 4000원
3 [NS몰] 데이즈온 오한진 초임계 알티지 오메가3 비타플러스 3개월 NS홈쇼핑 9,500원/무료 9,500.0 무료
4 [옥션] 리큐 진한겔 꿉꿉한냄새 싹 2.1L X 6 [20,930/무료배송] 지마켓/옥션 20,930/무료배송 20,930.0 무료배송

제품 가격 아웃라이어 확인 및 NaN 처리

특가 게시물 등록시 규칙을 지키지 않거나 가격을 여러번 적어 잘못 추출된 가격을 삭제하기 위해 상위 0.0014를 nan 값 처리 했다.

# 상위 0.0014 제외
cut = df["product_price"].quantile(0.9986)
print(f"기준 가격 {cut}")
temp = df[df["product_price"] > cut]
temp.sort_values("product_price", ascending=False)[["product_price", "price"]]
기준 가격 6050509.500005719
  product_price price
108936 7.495909e+17 749,590,887,040,974,160/무료
101994 9.600097e+14 96,000원,96,500원,97,000원/무료,무료,5장이상 구매시 무료
89044 3.570020e+14 35700,19800,18990/2500,3000
63422 3.083063e+14 30,830원,62,770원,33,620원/무료
76481 2.590028e+14 25900,27900,30900/무료배송
... ... ...
100576 9.701960e+06 970,1,960/2500
44753 8.891700e+06 889,1700/무료, 카드할인 791,360원, 자급제, 로켓배송
80368 7.690000e+06 769,000,0
34872 7.324760e+06 7324,760/무료
96789 6.510000e+06 651,0000/배송
top_index = temp.index
df.loc[top_index, "product_price"] = np.nan

배송비 정합성 확인 및 NaN 처리

배송비에 대한 단어들은 "무료", "무배" 등 여러 가지로 작성되어 있어서 무료배송을 뜻하는 단어를 포함하면 모두 0원으로 변경하고 그 외 단어는 NaN처리했다. 그리고 나머지 데이터는 숫자로 변경하였다

# 배송비 확인
df["shipping_cost"].value_counts()
무료                        53908
무배                        12669
unknown                   11391
무료배송                       9793
 무료                        4459
                          ...  
쿠폰받으면무료                       1
닌텐도 스위치                       1
2,500, 2만원이상 무료배송             1
와우회원무료, 카드할인20,720            1
 29,800원이상 무료,미만 5,000        1
Name: shipping_cost, Length: 4330, dtype: int64
def convert_shipping_cost(cost):
    """ 배송비 변환
    Args:
        cost - Str 형식의 배송비
    Returns:
        Nan : unknown이거나, 그 외 Str형 일때
        0 : 무료 배송일때
        int : 그 외 숫자형 일때
    """
    cost = cost.strip()
    if cost.find("무료") > -1:
        return "0"
    elif cost.find("무배") > -1:
        return "0"
    elif cost.replace("원", "").replace(",", "").replace("~", "").replace(".", "").isdigit():
        return cost.replace("원", "").replace(",", "").replace("~", "").replace(".", "")
    else:
        return np.NaN
df['shipping_cost'] = df['shipping_cost'].map(convert_shipping_cost)
df[['title', 'sales_channel', 'price', 'product_price', 'shipping_cost']].head(2)
  title sales_channel price product_price shipping_cost
0 [cj온스타일] 아이더 반팔 기능티 2장 (21,600원/무료) CJ 21,600원/무료 21600.0 0
1 [G마켓] PS5 디스크 에디션 갓오워 라그나로크 에디션(1218A) 지마켓/옥션 606,970/무료 606970.0 0

배송비 아웃라이어 확인 및 NaN 처리

배송비 정합성 체크 후 잘못 추출된 배송비가 있을 수 있었다. 배송비가 상위 0.002 이상 (약 2만원)은 NaN 처리를 해주었다.

# 배송비 아웃라이어 확인
df["shipping_cost"] = df["shipping_cost"].fillna(-1).astype(int).replace({-1: None})
# (상위 0.002 제외)
cut = df["shipping_cost"].quantile(0.998)
print(f"기준 가격 {cut}")
temp = df[df["shipping_cost"] > cut]
temp.sort_values("shipping_cost", ascending=False)[["shipping_cost", "price"]]
top_index = temp.index
df.loc[top_index, "shipping_cost"] = np.nan
기준 가격 20000.0

 

키워드 추출

특가 데이터 분석에 용이하게 하기 위해 Kiwi 패키지를 사용하여 제목에서 판매 채널과 가격 정보, 특수 문자를 제외하여 제목을 정제하였으며, 정제된 제목에서 불용어를 제외한 명사형 키워드를 추출하였다.
불용어의 기준은 의미를 모르거나, 자주 등장된 단어 중 필요 없다고 판단된 단어이다.

def clean_title(title):
    """ 제목 정체
    Args:
        title - Str 형식의 특가 게시물 제목
    Returns:
        title - Str 형식의 판매 채널, 제품 가격 정보, 특수 문자가 제외 된 제목
    """
    # 제목에서 판매 채널 제외
    title = re.sub(r'^\[([^\]]+)\]|\(([^\)]+)\)s*', '', title)
    # 제목에서 가격 정보 제외
    title = re.sub(r'\s*\[([^\]]+)\]|\(([^\)]+)\)$', '', title)

    return title
df['title'] = df['title'].astype(str)
df['real_title'] = df['title'].apply(clean_title)
df[['title', 'real_title']].head(2)
  title real_title
0 [cj온스타일] 아이더 반팔 기능티 2장 (21,600원/무료) 아이더 반팔 기능티 2장
1 [G마켓] PS5 디스크 에디션 갓오워 라그나로크 에디션(1218A) (606,97... PS5 디스크 에디션 갓오워 라그나로크 에디션

def noun_extractor(title):
    """ 명사 추출 및 불용어 처리 함수
    Args:
        title - Str 형식의 판매 채널, 제품 가격 정보, 특수 문자가 제외 된 제목
    Returns:
        results - List : title에서 지정된 불용어를 제외한 명사만 추출된 List
    """
    results = []
    try:
        result = kiwi.analyze(title)
    except:
        return results
    for token, pos, _, _ in result[0][0]:
        if len(token) != 1 and pos.startswith('N') and token not in stopwords:
                results.append(token)
    return results
# 불용어
stopwords = ["할인", "쿠폰", "상품", "무료", "스마일", "적용", "카드", "삼성", "세트", "클럽",
            "프로", "증정", "블랙", "인치", "스클", "박스", "에어", "세대", "무선", "랜드", "머니",
            "가능", "캡슐", "샤오미", "결제", "포인트", "구매", "추가", "최대", "배송", "프리미엄"]
tqdm.pandas()
kiwi = Kiwi()
df["keywords"] = df["real_title"].progress_apply(noun_extractor)
100%|█████████████████████████████████████████████████████████████████████████| 117980/117980 [00:23<00:00, 4979.96it/s]

 

인기 / 핫 게시물과 일반 게시물 라벨링

특가 데이터 분석을 인기/핫 게시물을 중점으로 할 것이므로, 인기/핫 게시물과 일반 게시물을 구별해주는 컬럼을 생성해주었다.

df.loc[df['pop'] == True, 'post_type'] = 'popular/hot'
df.loc[df['hot'] == True, 'post_type'] = 'popular/hot'
df['post_type'].fillna('general', inplace=True)

 

데이터 저장

전처리한 데이터를 csv 파일로 저장하여, 추후 특가 데이터 분석시 해당 전처리를 진행하지 않아도 되게 하였다. 또한 저장되는 파일명은 현재 시간을 자동으로 지정하여 실수로 다른 파일을 덮어쓰여 저장하지 않게 하였다.

# 데이터 csv 저장
now = str(datetime.now())
df.to_csv(f"./datas/{now}_preprocessing.csv", index=False)
728x90
반응형

'Data Science > Data Engineering' 카테고리의 다른 글

구글 colab과 vscode 연동하기  (0) 2024.12.13
Meta Tag를 사용한 뉴스기사 제목, 요약문, 이미지가져오기  (1) 2024.12.09
업비트 크롤링 (Crawling)  (1) 2024.12.03
아나콘다 가상환경 주피터랩에서 쉽게 쓰기  (1) 2024.11.30
뽐뿌 특가 게시판 크롤링하기  (0) 2024.11.23
'Data Science/Data Engineering' 카테고리의 다른 글
  • Meta Tag를 사용한 뉴스기사 제목, 요약문, 이미지가져오기
  • 업비트 크롤링 (Crawling)
  • 아나콘다 가상환경 주피터랩에서 쉽게 쓰기
  • 뽐뿌 특가 게시판 크롤링하기
Data Include Me
Data Include Me
AI, LLM, 머신러닝, 파이썬 등 최신 정보와 튜토리얼을 제공하는 데이터 사이언스 전문 블로그입니다.
  • Data Include Me
    Data Include Me
    Data Include Me
  • 전체
    오늘
    어제
    • 전체 (35)
      • AI (16)
        • Machine Learing (2)
        • Deep Learning (0)
        • Natural Language Processing (4)
        • Large Language Model (7)
        • Computer Vision (3)
      • Data Science (10)
        • Data Analysis (1)
        • Statistics & Math (3)
        • Data Engineering (6)
        • Data Visualization (0)
      • Programming Challenges (2)
        • Baekjoon (0)
        • Programmers (2)
        • HackerRank (0)
      • Development (7)
        • Cloud & DevOps (5)
        • Project (2)
  • 인기 글

  • 태그

    sympy
    Cloud Computing
    llm
    LangChain
    오블완
    티스토리챌린지
    Crawling
    Python
    mcp
    integral
  • 링크

    • Github
    • Linkedin
  • 반응형
  • hELLO· Designed By정상우.v4.10.1
Data Include Me
뽐뿌 특가 데이터 전처리 하기
상단으로

티스토리툴바