일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | ||||
4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 |
- 시각화
- 비즈니스 분석가
- BDA과정
- sql문제풀이
- 데이터분석
- 파이썬
- sql with
- 데이터분석가양성과정
- stratascratch
- 크롤링
- 태블로
- 논리적사고
- Python
- GA
- 국비지원
- for
- groupby
- SQL
- Tableau
- while
- SubQuery
- eda
- 패스트캠퍼스
- sql partition by
- 데이터베이스
- sql문제
- 데이터 분석을 위한 sql 레시피
- 데이터캠프
- 비즈니스분석가양성과정
- 데이터분석가 과정
- Today
- Total
원시인
[16일,17일,18일,19일,20일 ] BDA과정 EDA-Project 본문
[KDT] 패스트캠퍼스 비즈니스 데이터 분석가 양성과정 16일~20일 차
EDA 프로젝트
안녕하세요 일주일만에 돌아왔네요 ㅎㅎ
저번주 화요일부터 이번주 월요일까지 BDA과정 첫 프로젝트를 진행하느라 바쁜나날을 보내
글을 올리지 못했습니다.
EDA프로젝트인 만큼 원래 존재하던 데이터셋보다는 주제에 부합하는 데이터를 크롤링하고 정제, 시각화 및 분석하는 과정을 가졌습니다.
주제에 따른 문제정의를 명확하게 하지 못해 여러번 주제를 바꿔가며 주말까지 시간을 할애에 진행하였는데 일단 너무 좋은 팀원들을 만나고 저희가 우려했던 부분인 문제정의, 분석결과 등을 강사님, 다른 조원분들이 저희 생각과 다르게 좋게 봐주셔서 기분좋게 마무리 할 수 있었던거 같습니다.
아무런 정제가 되지 않은 rawdata를 가져와 분석에 알맞은 형태로 바꾸는 과정, 코드 구현에 있어 심여를 기울였기 때문에 그 점들을 봐주셨으면 감사하겠습니다. 그럼 저희가 진행했던 EDA 프로젝트 올려보겠습니다.
프로젝트 내용이 많아 다 올라가지 못하기 때문에 줄여서 업로드 하겠습니다.
BDA 2기 EDA 프로젝트 - 2조 중고신입

0. Problem Definition
MZ세대의 중고거래 확대와 함께 리셀시장이 급성장하여 특히 신발에 대한 거래량이 많아지고 있다.
주로 크림에서 리셀신발을 거래하는 것으로 보이는데, 새상품뿐만이 아닌 중고거래에서는 어떤 시세 변화 추이가 있고, 시세에 영향을 미치는 요인이 무엇인지 탐색해보고자 합니다.
분석 목표 : 중고시장에서의 리셀 신발의 거래 현황을 살펴보고, 시세에 영향을 미치는 요인이 무엇인지 탐색해보고자 한다.
<분석 배경>
중고시장 & 리셀시장¶
"2008년 4조원 규모에 불과했던 국내 중고 거래 시장은 2020년 약 20조원 규모로 성장"(통계청)하며, '롯데그룹'이 '중고나라'를 인수하는 등 대기업까지 잇따라 뛰어들고 있다. MZ세대는 이러한 중고시장의 성장을 주도했다. MZ세대는 '소비'보다 '경험'을 중요시하는 성향, 중고품을 바라보는 인식의 변화, 자원순환의 기여 등의 이유로 중고거래를 선호하고 있다.
https://img.hankyung.com/photo/202108/AA.27353032.1.jpg
최근, MZ세대의 중고거래 확대와 함께 리셀시장도 급성장하며 리셀 플랫폼에 대한 관심이 높아지고 있다. "2025년에는 스니커즈 리셀 시장 규모만 약 7조원에 이를 것으로 예상(미국 투자은행 코웬)"하고 있다. 명품에 대한 MZ세대의 관심이 증대되고 있으며, 명품을 하나의 투자방식으로 바라보고 있기 때문이다.
"중고 혹은 새상품을 개인이 개인과 거래"한다는 부분에서 중고시장과 리셀시장은 큰 공통점을 가지고 있다. 하지만 리셀플랫폼은 그 특성상 리셀제품의 시세를 공개하고 있지만 중고플랫폼은 정확한 시세를 파악하기가 어렵다. 이러한 유사점에서 착안하여 리셀 플랫폼 외부에서 거래되는 중고플랫폼의 한정판 상품(or 신발 or 스니커즈) 데이터를 분석하여 리셀 플랫폼 바깥의 공급과 가격의 추이(?)등을 알아본다. 이를 통해 최종적으로 리셀 제품의 합리적 소비에 기여하고자 한다.
번개장터
"취향을 잇는 거래, 번개장터"라는 키메시지처럼 한정판, 굿즈와 같은 취향을 반영하는 물품의 거래가 비교적 활발하다. 이 때문에 10대가 가장 많이 이용하는 중고거래 플랫폼으로, 가장 많이 사용하는 쇼핑 어플 3위에 오르기도 했다. 2020년 기준, 중고거래 플랫폼 순이용자수 2위, 총 거래액 약 1조 3천억을 기록하며 중고나라에 이어 2위를 기록했다. 이를 통해 리셀거래가 활발한 중고거래 플랫폼으로써 대표성을 가질 수 있다고 판단하여 선정했다. (https://www.hankookilbo.com/News/Read/A2020113009180002766)
크림 KREAM
https://img.hankyung.com/photo/202108/AA.27353032.1.jpg
국내 스니커즈 리셀 시장 점유율 1위 리셀 플랫폼. 전체 가입자 수는 160만 명으로 월간순이용자는 약 45만명으로 추산된다. 가입자 중 2030회원이 80%를 차지할 정도로 한정판 스니커즈에 관심이 많은 MZ세대들이 가장 많이 이용하는 플랫폼이라 할 수 있다.
1. Data Collection (Crawling)
1.1 수집 개요
- 사용된 파이썬 패키지
- requests / json / time
- pandas / numpy
- 데이터 출처 : 번개장터 신발데이터 (https://m.bunjang.co.kr/)
- 데이터 유형 : json 파일
- 데이터 사이즈 : 32,610 개의 데이터에서 전처리 시행
- 데이터 선정 기준
- 신발전문 거래 사이트 크림(Kream)에서 거래 상위 품목
- 판매 등록 건수 100이상의 품목
- 데이터 품목
- 나이키 : 나이키 덩크 하이 / 나이키 덩크 로우
- 조던1 : 조던1 하이 / 조던1 미드 / 조던1 로우
- 뉴발란스 : 뉴발란스 993 / 뉴발란스 992
- 상품 = '나이키 덩크로우 골든로드', '나이키 덩크로우 라이트본','나이키 덩크로우 바시티그린', '나이키 덩크로우 범고래','나이키 덩크로우 유니버시티블루', '나이키 덩크로우 코스트','나이키 덩크하이 네이비','나이키 덩크하이 범고래','나이키 덩크하이 오렌지','뉴발란스 992 그레이','뉴발란스 992 네이비', '뉴발란스 992 블랙그레이','뉴발란스 992 화이트실버','뉴발란스 993 그레이','뉴발란스 993 네이비','뉴발란스 993 블랙','조던 1 로우 스타피쉬', '조던 1 로우 울프그레이','조던 1 로우 트레비스 스캇''조던1 미드 그레이포그','조던1 미드 스모크그레이','조던1 미드 울프그레이', '조던1 미드 짐레드','조던1 하이 다크모카','조던1 하이 스모크그레이', '조던1 하이 하이퍼로얄'
1.2 크롤링
- 번개장터 데이터를 수집하기 위한 크롤러를 정의해서 사용합니다.
- 크롤링하는 과정을 함수화(모듈화)하여 편의성을 높입니다.
- 키워드 리스트를 만들어 for문을 수행합니다.
1.2.0 패키지 불러오기
import requests # 크롤링을 위함
import json # json파일을 다루기위함
import time # 크롤링에 딜레이를 두기 위함
import pandas as pd
import numpy as np
1.2.1 Crawler 함수 생성
- keyword와 url, n을 인자로 받아 크롤링을 진행한다.
- keyword와 url은 리스트이고, n은 1페이지부터 n페이지까지 크롤링해준다. (default=20 : 20페이지 * 100 = 2000개)
- 데이터프레임을 반환받으며, 크롤러 함수를 변수에 선언해주어야한다.
# 번개장터 데이터를 크롤링 할 수 있는 함수, keyword와 n을 인자로 받는다.
def crawler(keyword, n=20):
df = pd.DataFrame()
for i in range(0,n):
# 차단막는 코드, 랜덤으로 time.sleep 지정
seed = np.random.randint(100)
np.random.seed(seed)
a = np.random.randint(3)
time.sleep(a)
url_formating = url.format(keyword, i) # url에 포맷팅을 적용한다.
info = {
'referer': 'https://m.bunjang.co.kr/',
'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.54 Safari/537.36'
} # info는 변화없음
print(f'{i}th, {(i+1)*100}개째 상품을 크롤링 중')
# requests로 데이터 요청하기
resp = requests.get(url_formating, headers = info)
if resp.status_code == requests.codes.ok:
data = json.loads(resp.text)
next_df = pd.DataFrame(data['list'])
df = pd.concat([df, next_df])
else:
print(f'{i}번째 요청이 잘못되었습니다.')
pass # 넘겨준다.
print('크롤링 완료!!')
result = df.reset_index(drop=True)
return result
1.2.2 Keywords, url 선언
- Crawler 함수에 들어갈 인자(keywords, url)를 선언한다.
keywords = ['나이키 덩크로우', '나이키 덩크하이', '조던1 로우', '조던1 미드', '조던1 하이', '뉴발란스 993', '뉴발란스 992']
url = 'https://api.bunjang.co.kr/api/1/find_v2.json?q={}&order=date&page={}&request_id=20211110194853&stat_uid=77848616&token=c63b703f121940d189c222b3335d80ed&stat_device=w&n=100&stat_category_required=1&req_ref=search&version=4'
1.2.3. for문을 이용한 모든 키워드 Crawling
- 위에서 선언한 Crawler함수와 keywords리스트, url을 이용하여 모든 데이터를 크롤링한다.
[사용법]
- keywords 리스트에 검색어를 차례로 입력한다.
- url은 keyword에 해당되는 Request URL을 입력하였고, 이는 키워드가 달라져도 변함이 없다.
- url은 개발자도구 Network 탭에서 페이지클릭 후 find_v2.json파일에 해당되는 Request URL을 가져온다.
- find_v2.json?q={} => 상품명을 {}로 변환한다 (함수 내에서 포맷팅을 위해)
- order=date&page={} => 페이지를 {}로 변환한다 (함수 내에서 포맷팅을 위해)
- 검색 시점에 해당되는 request_id가 있으나 시간데이터로 모두 같은 url을 사용함으로써 통일시킨다. (2021-11-11기준)
# 키워드 리스트에 있는 모든 데이터를 크롤링한다.
rawdata = pd.DataFrame()
for keyword in keywords:
print(f'{keyword} 크롤링 시작!')
next_df = crawler(keyword, n=100)
rawdata = pd.concat([rawdata, next_df])
print('----'*10)
print('모든 신발 크롤링 완료!')
rawdata = rawdata.reset_index(drop=True)
1.2.4 데이터 저장
- 크롤링 완료된 데이터를 rawdata.csv에 저장한다.
rawdata.to_csv('./data/rawdata.csv')
2. Data Preprocessing
2.1 전처리 개요
- 각각의 제품마다 특성이 달랐기 때문에 제품별 전처리과정을 간단히 거친후 Merge하는 과정을 거쳤다.
- 다만, 기준은 모두 동일했고, 아래 과정들을 거쳐서 분석에 필요한 최종 데이터를 완성하였다.
- rawdata 전체에 적용하는 전처리
- 컬럼 추가 : datetime, date, location1, locatoin2, location3
- 로우 중복제거
- 제품별 특성을 고려한 전처리
- 컬럼 추가 : size, product_name, brand, product_type, color, release_price
- EDA하기 편리한 데이터로 변환
- 사용할 컬럼만 추출
- 컬럼에 조건 부여
2.2 전처리
2.2.0 패키지 불러오기
import pandas as pd
import numpy as np
from datetime import datetime, timedelta
import warnings
warnings.filterwarnings(action='ignore') # 빨간색 경고메시지 무시해줌
pd.options.display.max_rows=100
pd.options.display.max_columns=100
2.2.1 데이터 로드 (rawdata)
rawdata = pd.read_csv("./data/rawdata.csv", index_col = 0).reset_index(drop=True)
print(rawdata.shape)
rawdata.head(1)
(32610, 41)
ad | bizseller | checkout | contact_hope | free_shipping | is_adult | is_super_up_shop | location | max_cpc | name | num_comment | num_faved | only_neighborhood | outlink_url | pid | price | product_image | pu_id | ref_campaign | ref_code | ref_medium | ref_content | ref_source | status | style | super_up | tag | uid | update_time | used | bun_pay_filter_enabled | imp_id | ad_ref | faved | datetime | year | month | day | week | weekday | hour | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | False | False | False | False | False | False | NaN | 서울특별시 영등포구 문래동 | NaN | 나이키 덩크하이 오렌지 블레이즈 | 0 | 1 | False | NaN | 169596525 | 210000 | https://media.bunjang.co.kr/product/169596525_... | NaN | NaN | soldout_test_v2:B | NaN | NaN | NaN | 0 | NaN | NaN | NaN | 75765744 | 1636894443 | 2 | True | 2b9a619107130ec5e493 | NaN | False | 2021-11-14 21:54:03 | 2021.0 | 11.0 | 14.0 | 45.0 | 6.0 | 21.0 |
2.2.2 rawdata 전체에 적용하는 전처리
- datetime : update_time에서 파생 ex) 2021-11-05 21:54:03
- date : datetime 파생 ex) 2021-11-05
- location1 : location에서 파생 ex) 서울특별시
- location2 : location에서 파생 ex) 강남구
- 중복제거
2.2.2.1 date, datetime
- 기존의 update_time 컬럼 : 1970-01-01 09:00 이후로 초를 카운트한 값이다.
- 1970-01-01 09:00라는 기준에서 5분 이내의 오차가 존재한다. (큰 문제 없음)
- update_time 컬럼을 datetime과 timedelta를 이용해 날짜로 변환한다.
- 새로 생성된 컬럼
- datetime : timedelta형식의 날짜데이터
- date : datetime에서 날짜만 뽑아낸 컬럼
def get_date(x):
return datetime(1970,1,1,9) + timedelta(seconds=int(x))
def datetime_maker(df):
df['datetime'] = df['update_time'].apply(get_date)
return df
### 시간,분,초 삭제하고 새로운 date 컬럼 생성
def dt_index(x):
return str(x)[:10]
rawdata = datetime_maker(rawdata)
print(rawdata.shape)
rawdata.tail(1)
(32610, 41)
ad | bizseller | checkout | contact_hope | free_shipping | is_adult | is_super_up_shop | location | max_cpc | name | num_comment | num_faved | only_neighborhood | outlink_url | pid | price | product_image | pu_id | ref_campaign | ref_code | ref_medium | ref_content | ref_source | status | style | super_up | tag | uid | update_time | used | bun_pay_filter_enabled | imp_id | ad_ref | faved | datetime | year | month | day | week | weekday | hour | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
32609 | False | False | False | False | False | False | NaN | 경기도 화성시 동탄3동 | NaN | [290] 조던1 미드 마젠타 팝니다 -새제품 | 0 | 1 | False | NaN | 128192654 | 159000 | https://media.bunjang.co.kr/product/128192654_... | NaN | NaN | soldout_test_v2:B | NaN | NaN | NaN | 3 | NaN | NaN | 조던1 미드 마젠타 신상품 290 | 6017020 | 1595131995 | 13 | False | 2f73618cd52203229b3c | NaN | False | 2020-07-19 13:13:15 | 2020.0 | 7.0 | 19.0 | 29.0 | 6.0 | 13.0 |
rawdata["date"] = rawdata["datetime"].apply(dt_index)
print(rawdata.shape)
rawdata.tail(1)
(32610, 42)
ad | bizseller | checkout | contact_hope | free_shipping | is_adult | is_super_up_shop | location | max_cpc | name | num_comment | num_faved | only_neighborhood | outlink_url | pid | price | product_image | pu_id | ref_campaign | ref_code | ref_medium | ref_content | ref_source | status | style | super_up | tag | uid | update_time | used | bun_pay_filter_enabled | imp_id | ad_ref | faved | datetime | year | month | day | week | weekday | hour | date | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
32609 | False | False | False | False | False | False | NaN | 경기도 화성시 동탄3동 | NaN | [290] 조던1 미드 마젠타 팝니다 -새제품 | 0 | 1 | False | NaN | 128192654 | 159000 | https://media.bunjang.co.kr/product/128192654_... | NaN | NaN | soldout_test_v2:B | NaN | NaN | NaN | 3 | NaN | NaN | 조던1 미드 마젠타 신상품 290 | 6017020 | 1595131995 | 13 | False | 2f73618cd52203229b3c | NaN | False | 2020-07-19 13:13:15 | 2020.0 | 7.0 | 19.0 | 29.0 | 6.0 | 13.0 | 2020-07-19 |
2.2.2.2 location1, location2
rawdata['location'] = rawdata['location'].fillna('NULL')
def first_split(x):
first = x.find(' ')
return x[:first]
def second_split(x):
first = x.find(' ')
second = x.find(' ', first+1)
return x[first+1:second]
def third_split(x):
first = x.find(' ')
second = x.find(' ', first+1)
third = x.find(' ', second+1)
return x[second+1:]
rawdata['location1'] = rawdata['location'].apply(first_split)
rawdata['location2'] = rawdata['location'].apply(second_split)
rawdata['location3'] = rawdata['location'].apply(third_split)
2.2.2.3 중복제거
- 크롤링 과정에서 상품이 등록되면 한칸씩 밀리면서 같은 상품이 두번 크롤링되는 경우가 있다.
- pid(상품명)를 기준으로 중복을 제거한다. (상품명은 unique id이기 때문)
def deduplicator(df):
df = df.drop_duplicates(['pid'], keep='first')
return df.reset_index(drop=True)
print(len(rawdata), len(deduplicator(rawdata)))
32610 31113
rawdata = deduplicator(rawdata)
print(rawdata.shape)
rawdata.tail(1)
(31113, 45)
ad | bizseller | checkout | contact_hope | free_shipping | is_adult | is_super_up_shop | location | max_cpc | name | num_comment | num_faved | only_neighborhood | outlink_url | pid | price | product_image | pu_id | ref_campaign | ref_code | ref_medium | ref_content | ref_source | status | style | super_up | tag | uid | update_time | used | bun_pay_filter_enabled | imp_id | ad_ref | faved | datetime | year | month | day | week | weekday | hour | date | location1 | location2 | location3 | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
31112 | False | False | False | False | False | False | NaN | 경기도 화성시 동탄3동 | NaN | [290] 조던1 미드 마젠타 팝니다 -새제품 | 0 | 1 | False | NaN | 128192654 | 159000 | https://media.bunjang.co.kr/product/128192654_... | NaN | NaN | soldout_test_v2:B | NaN | NaN | NaN | 3 | NaN | NaN | 조던1 미드 마젠타 신상품 290 | 6017020 | 1595131995 | 13 | False | 2f73618cd52203229b3c | NaN | False | 2020-07-19 13:13:15 | 2020.0 | 7.0 | 19.0 | 29.0 | 6.0 | 13.0 | 2020-07-19 | 경기도 | 화성시 | 동탄3동 |
2.2.3 제품별 특성을 고려한 전처리
제품별 name컬럼과 출시가격 등의 특성이 다르기에 각각 적용해주고 합쳐준다
- size : name컬럼에서 파생, 사이즈 데이터 ex) 260
- product_name : name컬럼에서 파생, 제품의 이름 ex) 나이키 덩크로우 범고래
- brand : name컬럼에서 파생, 제품의 브랜드 ex) 나이키
- product_type : name컬럼에서 파생, 제품의 유형 ex) 나이키 덩크로우
- color : name컬럼에서 파생, 제품의 색상 ex) 범고래
- release_price : 제품의 출고가
예시) 조던1 로우 스타피쉬 데이터
# target_string에 포함되는 단어들을 가지면 상품을 추출한다. (나이키 덩크로우 라이트본)
target_string = ['조던', '1', '로우', '스타피쉬']
product = rawdata[rawdata['name'].map(lambda x: all(string in x for string in target_string))]
product = product.reset_index(drop=True)
print(product.shape)
product.head(1)
(390, 45)
ad | bizseller | checkout | contact_hope | free_shipping | is_adult | is_super_up_shop | location | max_cpc | name | num_comment | num_faved | only_neighborhood | outlink_url | pid | price | product_image | pu_id | ref_campaign | ref_code | ref_medium | ref_content | ref_source | status | style | super_up | tag | uid | update_time | used | bun_pay_filter_enabled | imp_id | ad_ref | faved | datetime | year | month | day | week | weekday | hour | date | location1 | location2 | location3 | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | False | False | False | False | False | False | NaN | 서울특별시 마포구 연남동 | NaN | [280] 조던1 로우 스타피쉬 | 0 | 1 | False | NaN | 169150975 | 340000 | https://media.bunjang.co.kr/product/169150975_... | NaN | NaN | soldout_test_v2:B | NaN | NaN | NaN | 0 | NaN | NaN | 나이키 조던1 로우 280 덩크 | 3385117 | 1636602096 | 1 | False | 2f73618d081804c4c128 | NaN | False | 2021-11-11 12:41:36 | 2021.0 | 11.0 | 11.0 | 45.0 | 3.0 | 12.0 | 2021-11-11 | 서울특별시 | 마포구 | 연남동 |
2.2.3.1 size 컬럼
import re
# name에서 숫자만 추출
number = []
for i in range(0, len(product)):
number.append(re.sub(r'[^0-9]','',product['name'].values[i]))
product["size"] = pd.DataFrame(number)
# 에어조던1의 1을 없애기
def strip_one(x):
return x.strip('1')
product['size'] = product['size'].apply(strip_one)
set(product['size'])
{'',
'0775801240',
'079080',
'225',
'230',
'235',
'235240',
'23565',
'240',
'245',
'250',
'250260',
'250270275280290',
'250280',
'255',
'255265',
'260',
'260265',
'260265270',
'260270275',
'265',
'270',
'270079080',
'270285',
'27032',
'275',
'275275280',
'280',
'2802705',
'280285',
'285',
'290',
'295300',
'300',
'310',
'330'}
# 1단계
def size (x):
return x[0:3]
product['size'] = product['size'].apply(size)
# # 2단계
# int 변환 후 150 ~ 350 사이값
product['size'] = product['size'].replace('','0') #string
product['size'] = product['size'].astype(int)
product = product[(product['size'] >= 200) & (product['size'] <= 350)]
# 3단계
# 뒷자리 5로 나누어 떨어지도록함. (사이즈는 5단위이기 때문)
product = product[product['size'] % 5 == 0]
set(product['size'])
{225,
230,
235,
240,
245,
250,
255,
260,
265,
270,
275,
280,
285,
290,
295,
300,
310,
330}
print(product.shape)
product.head(1)
(349, 46)
ad | bizseller | checkout | contact_hope | free_shipping | is_adult | is_super_up_shop | location | max_cpc | name | num_comment | num_faved | only_neighborhood | outlink_url | pid | price | product_image | pu_id | ref_campaign | ref_code | ref_medium | ref_content | ref_source | status | style | super_up | tag | uid | update_time | used | bun_pay_filter_enabled | imp_id | ad_ref | faved | datetime | year | month | day | week | weekday | hour | date | location1 | location2 | location3 | size | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | False | False | False | False | False | False | NaN | 서울특별시 마포구 연남동 | NaN | [280] 조던1 로우 스타피쉬 | 0 | 1 | False | NaN | 169150975 | 340000 | https://media.bunjang.co.kr/product/169150975_... | NaN | NaN | soldout_test_v2:B | NaN | NaN | NaN | 0 | NaN | NaN | 나이키 조던1 로우 280 덩크 | 3385117 | 1636602096 | 1 | False | 2f73618d081804c4c128 | NaN | False | 2021-11-11 12:41:36 | 2021.0 | 11.0 | 11.0 | 45.0 | 3.0 | 12.0 | 2021-11-11 | 서울특별시 | 마포구 | 연남동 | 280 |
2.2.3.2 product_name 컬럼
product['product_name'] = '조던1 로우 스타피쉬' # 제품명 입력.
2.2.3.3 brand컬럼
product['brand'] = '조던1' # 브랜드 입력.
2.2.3.4 product_type 컬럼
product['product_type'] = '조던1 로우' # 제품유형
2.2.3.5 color 컬럼
product['color'] = '스타피쉬' # 색상 입력.
2.2.3.6 release_price
product['release_price'] = 159000 # 정발가입력
2.2.4 제품별 저장
- 제품별로 데이터를 저장하고,
- 데이터를 모두 합쳐 EDA를 위한 데이터를 만든다.
# product.to_csv('./data/jodan1_low_starfish.csv')
2.2.3 EDA를 위한 전처리
- 제품별 특성을 반영하고 merge를 마친 preprocessed_data를 불러와서 추가적인 전처리를 진행한다.
- EDA의 편의성을 높이기 위해 추가적인 전처리를 진행해준다.
- 필요한 컬럼만 추출 및 컬럼 순서 설정
- 빠진 데이터가 존재하면 채우기
- date : 2020-11-11~2021-11-11
- status : 0 = 판매중 / 1 = 예약완료 / 3 = 판매완료 변경
- used : 1, 13 = 중고 / 2 = 새상품 변경
- abnormal_price 컬럼 생성
- abnormal_price 컬럼 = 1만원 이하, 500만원 이상, 천원 단위의 숫자가 아닌 것(ex. 2222)
- abnormal_price는 판매자가 의도적으로 검색노출을 늘리기 위한 수단으로 price를 사용했을 가능성이 있음.
- name : "삽니다", "구매" 구매요청 게시글 row 제거
- release_price, size, uid, pid 데이터 형식 변환
2.2.3.1 전처리완료된 데이터 로드
- 2.2.2과정에서 각 제품별 특성을 반영한 preprocessed_data를 불러온다.
data = pd.read_csv('./data/preprocessed_data.csv', index_col=0).reset_index(drop=True)
print(data.shape)
data.tail(1)
(6916, 20)
location | name | num_faved | pid | price | status | uid | update_time | used | datetime | date | product_name | brand | product_type | color | size | release_price | location1 | location2 | location3 | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
6915 | 전라남도 여수시 여서동 | 조던1로우 260 트레비스 스캇 | 0 | 161653134 | 1800000 | 3 | 6165763 | 1628829698 | 1 | 2021-08-13 13:41:38 | 2021-08-13 | 조던 1 로우 트레비스 스캇 | 조던 | 조던 1 로우 | 트레비스 스캇 | 260 | 189000.0 | 전라남도 | 여수시 | 여서동 |
2.2.3.2 필요한 컬럼만 추출 및 컬럼 순서 설정
columns = ['pid', 'uid', 'product_name', 'brand', 'product_type', 'color', 'size',
'price', 'release_price', 'datetime', 'date', 'location1', 'location2',
'status', 'used', 'name']
data = data[columns]
2.2.3.3 Null값 채우기
- relaese_price 채우기
- location2는 상관없음
# 결측값이 있는 컬럼만 추출
for col in data.columns:
if np.round(data[col].isnull().sum() / len(data[col] ),2) > 0:
print(col, '결측값 존재')
release_price 결측값 존재
location2 결측값 존재
data.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 6916 entries, 0 to 6915
Data columns (total 16 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 pid 6916 non-null int64
1 uid 6916 non-null int64
2 product_name 6916 non-null object
3 brand 6916 non-null object
4 product_type 6916 non-null object
5 color 6916 non-null object
6 size 6916 non-null int64
7 price 6916 non-null int64
8 release_price 6663 non-null float64
9 datetime 6916 non-null object
10 date 6916 non-null object
11 location1 6916 non-null object
12 location2 6881 non-null object
13 status 6916 non-null int64
14 used 6916 non-null int64
15 name 6916 non-null object
dtypes: float64(1), int64(6), object(9)
memory usage: 864.6+ KB
# release_price 없는 조던1 로우 스타피쉬에 159000 추가
# rawdata.loc[rawdata['release_price'].isnull(), 'product_name'].value_counts()
data.loc[data['release_price'].isnull(), 'release_price'] = 159000
data[data['release_price'].isnull()]
pid | uid | product_name | brand | product_type | color | size | price | release_price | datetime | date | location1 | location2 | status | used | name |
---|
2.2.3.4 date : 2020-11-11 ~ 2021-11-11
data['date'] = pd.to_datetime(data['date'])
# rawdata['date'].value_counts().sort_index().head(5)
data = data[data['date'] > '2020-11-11'].reset_index(drop=True)
print(data.shape)
data.tail(1)
(6914, 16)
pid | uid | product_name | brand | product_type | color | size | price | release_price | datetime | date | location1 | location2 | status | used | name | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
6913 | 161653134 | 6165763 | 조던 1 로우 트레비스 스캇 | 조던 | 조던 1 로우 | 트레비스 스캇 | 260 | 1800000 | 189000.0 | 2021-08-13 13:41:38 | 2021-08-13 | 전라남도 | 여수시 | 3 | 1 | 조던1로우 260 트레비스 스캇 |
2.2.3.5 status : 0 = 판매중 / 1 = 예약완료 / 3 = 판매완료 변경
data.loc[data['status'] == 0, 'status'] = '판매중'
data.loc[data['status'] == 1, 'status'] = '예약완료'
data.loc[data['status'] == 3, 'status'] = '판매완료'
data['status'].value_counts()
판매완료 4517
판매중 2079
예약완료 318
Name: status, dtype: int64
2.2.3.6 used : 1, 13 = 중고 / 2 = 새상품 변경
data.loc[(data['used'] == 1) | (data['used'] == 13), 'used'] = '중고'
data.loc[data['used'] == 2, 'used'] = '새상품'
data['used'].value_counts()
중고 3870
새상품 3044
Name: used, dtype: int64
2.2.3.7 abnormal_price 컬럼 생성
- abnormal_price 컬럼 = 1만원 이하, 500만원 이상, 천원 단위의 숫자가 아닌 것(ex. 2222)
- abnormal_price는 판매자가 의도적으로 검색노출을 늘리기 위한 수단으로 price를 사용했을 가능성이 있음.
def make_bool(x):
if x % 1000 != 0:
return True
elif x < 10000:
return True
elif x > 5000000:
return True
else:
return False
data['abnormal_price'] = data['price'].apply(make_bool)
2.2.3.8 "삽니다", "구매" 구매요청 게시글 제거
- 삽니다, 구매요청 게시글은 판매용이 아니기 때문에 제거
idx_buying = data[data['name'].str.contains('구매|삽니다')].index
data = data.drop(idx_buying).reset_index(drop=True)
print(data.shape)
data.tail(1)
(6825, 17)
pid | uid | product_name | brand | product_type | color | size | price | release_price | datetime | date | location1 | location2 | status | used | name | abnormal_price | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
6824 | 161653134 | 6165763 | 조던 1 로우 트레비스 스캇 | 조던 | 조던 1 로우 | 트레비스 스캇 | 260 | 1800000 | 189000.0 | 2021-08-13 13:41:38 | 2021-08-13 | 전라남도 | 여수시 | 판매완료 | 중고 | 조던1로우 260 트레비스 스캇 | False |
2.2.3.9 데이터 형식 변환
data["release_price"] = data["release_price"].astype(int)
data["pid"] = data["pid"].astype(object)
data["uid"] = data["uid"].astype(object)
data["size"] = data["size"].astype(object)
5. EDA
5.0 분석 환경 세팅
5.0.1 패키지 불러오기
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
%matplotlib inline
import warnings
warnings.filterwarnings(action='ignore')
import matplotlib
matplotlib.rc('font', family='AppleGothic')
matplotlib.rc('axes', unicode_minus=False)
from IPython.display import set_matplotlib_formats
set_matplotlib_formats('retina')
# 그래프 틀 변경
plt.rcParams['axes.unicode_minus'] = False
sns.set(font_scale = 1)
plt.style.use(['fivethirtyeight'])
pd.set_option('display.max_columns', None)
pd.options.display.max_rows=100
pd.options.display.max_columns=100
pd.set_option('display.float_format','{:.0f}'.format)
# pd.reset_option('display.float_format')
# # 한글 폰트 설정
# from matplotlib import font_manager, rc
# f_path = "c:/Windows/Fonts/malgun.ttf"
# font_name = font_manager.FontProperties(fname=f_path).get_name()
# rc('font', family=font_name)
# plt.rc('font', family='gulrim')
import os
if os.name == 'posix':
plt.rc("font", family="AppleGothic")
else :
plt.rc("font", family="Malgun Gothic")
5.1 데이터 살펴보기
5.1.0 Data Info
- 컬럼
- pid : 등록상품 고유번호
- uid : 판매자 고유번호
- product_name : 제품명
- brand : 브랜드
- product_type : 제품 유형
- color : 색깔
- size : 사이즈
- price : 등록된 가격
- release_price : 출고가
- datetime : 상품 등록 시점
- date : 상품 등록 날짜
- location1 : 광역시/도
- location2 : 시/구
- status : 제품 상태
- used : 중고 여부
- name : 상품 등록된 이름
- abnormal_price : 비정상적 가격인지
data.head(1)
pid | uid | product_name | brand | product_type | color | size | price | release_price | datetime | date | location1 | location2 | status | used | name | abnormal_price | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 169499712 | 10473233 | 나이키 덩크하이 네이비 | 나이키 | 나이키 덩크하이 | 네이비 | 265 | 190000 | 129000 | 2021-11-11 19:58:01 | 2021-11-11 | 인천광역시 | 남동구 | 판매중 | 새상품 | (가격내림)나이키 덩크 하이 레트로 챔피언쉽 네이비 265사이즈 팔아요. | False |
pd.set_option('display.float_format','{:.0f}'.format)
data.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 6825 entries, 0 to 6824
Data columns (total 17 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 pid 6825 non-null object
1 uid 6825 non-null object
2 product_name 6825 non-null object
3 brand 6825 non-null object
4 product_type 6825 non-null object
5 color 6825 non-null object
6 size 6825 non-null object
7 price 6825 non-null int64
8 release_price 6825 non-null int64
9 datetime 6825 non-null object
10 date 6825 non-null datetime64[ns]
11 location1 6825 non-null object
12 location2 6790 non-null object
13 status 6825 non-null object
14 used 6825 non-null object
15 name 6825 non-null object
16 abnormal_price 6825 non-null bool
dtypes: bool(1), datetime64[ns](1), int64(2), object(13)
memory usage: 859.9+ KB
data.describe()
price | release_price | |
---|---|---|
count | 6825 | 6825 |
mean | 993001 | 157306 |
std | 24487399 | 48673 |
min | 0 | 119000 |
25% | 220000 | 119000 |
50% | 290000 | 129000 |
75% | 340000 | 199000 |
max | 999999999 | 259000 |
data.select_dtypes(include=['object']).describe()
pid | uid | product_name | brand | product_type | color | size | datetime | location1 | location2 | status | used | name | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
count | 6825 | 6825 | 6825 | 6825 | 6825 | 6825 | 6825 | 6825 | 6825 | 6790 | 6825 | 6825 | 6825 |
unique | 6825 | 5280 | 25 | 3 | 7 | 19 | 26 | 6696 | 21 | 164 | 3 | 2 | 5626 |
top | 162365440 | 7623840 | 나이키 덩크로우 범고래 | 나이키 | 나이키 덩크로우 | 범고래 | 270 | 2021-11-11 20:41:56 | 서울특별시 | NUL | 판매완료 | 중고 | 나이키 덩크로우 범고래 270 |
freq | 1 | 57 | 1955 | 3406 | 2845 | 2220 | 899 | 4 | 1930 | 771 | 4483 | 3805 | 50 |
5.1.0.1 데이터 복사 (백업))
df = data.copy()
5.1.1 price
df.groupby("brand")["price"].describe() # 이상값을 제거해야한다. => 분석시에 시행함.(abnormal_price컬럼 + 5%제거)
count | mean | std | min | 25% | 50% | 75% | max | |
---|---|---|---|---|---|---|---|---|
brand | ||||||||
나이키 | 3406 | 969278 | 24434164 | 0 | 230000 | 290000 | 325000 | 999999999 |
뉴발란스 | 1209 | 460136 | 6387678 | 0 | 220000 | 289000 | 350000 | 222222222 |
조던 | 2210 | 1321072 | 30158325 | 0 | 209000 | 300000 | 369000 | 999999999 |
5.1.2 brand
df['brand'].value_counts()
나이키 3406
조던 2210
뉴발란스 1209
Name: brand, dtype: int64
sns.countplot(df['brand'])
<AxesSubplot:xlabel='brand', ylabel='count'>
5.1.3 product_type
df['product_type'].value_counts()
나이키 덩크로우 2845
조던1 하이 1007
뉴발란스 992 871
조던1 미드 685
나이키 덩크하이 561
조던 1 로우 518
뉴발란스 993 338
Name: product_type, dtype: int64
plt.figure(figsize=(10,4))
sns.countplot(df['product_type'].sort_values(ascending =True))
plt.ylim(0,3000)
(0.0, 3000.0)
### 파이차트
# 제품별 df 프레임 구축
df_n = df[df["brand"] == "나이키"]
df_n_value = df_n["product_name"].value_counts()
fig , ax = plt.subplots(figsize = (13,10))
wedgeprops={'width': 0.7, 'edgecolor': 'w', 'linewidth': 5}
colors = ["gray","silver","gold","blue","yellow","navy","skyblue", "green", "orange"]
ax.pie(x = df_n["product_name"].value_counts(), labels = df_n_value.index, autopct='%.1f%%'
, startangle=260, counterclock=False, wedgeprops=wedgeprops, textprops ={'size' :13}, colors = colors)
# autopct는 부채꼴 안에 표시될 숫자의 형식을 지정합니다. 소수점 한자리까지 표시하도록 설정했습니다.
# startangle는 부채꼴이 그려지는 시작 각도를 설정합니다.
# counterclock=False로 설정하면 시계 방향 순서로 부채꼴 영역이 표시됩니다
plt.show()
# 뉴발
df_b = df[df["brand"]== "뉴발란스"]
df_b_value = df_b["product_name"].value_counts()
fig , ax = plt.subplots(figsize = (13,10))
wedgeprops={'width': 0.7, 'edgecolor': 'w', 'linewidth': 5}
colors = ["gray","skyblue","lightgray","silver","gray","skyblue","lightgray", "brown", "skyblue"]
ax.pie(x = df_b["product_name"].value_counts(), labels = df_b_value.index, autopct='%.1f%%'
, startangle=260, counterclock=False, wedgeprops=wedgeprops, textprops ={'size' :15}, colors = colors)
# autopct는 부채꼴 안에 표시될 숫자의 형식을 지정합니다. 소수점 한자리까지 표시하도록 설정했습니다.
# startangle는 부채꼴이 그려지는 시작 각도를 설정합니다.
# counterclock=False로 설정하면 시계 방향 순서로 부채꼴 영역이 표시됩니다
fig.tight_layout() # 메소드는 서브 플롯간에 올바른 간격을 자동으로 유지합니다.
plt.subplots_adjust(left=0.1, right=0.95, bottom=0.1, top=0.95, wspace=0.7, hspace=0.5) # #subplot 간 간격 조절
plt.show()
# 조던
df_j = df[df["brand"]== "조던"]
df_j_value = df_j["product_name"].value_counts()
df_j_value.index
Index(['조던1 하이 하이퍼로얄', '조던1 미드 짐레드', '조던1 하이 다크모카', '조던 1 로우 스타피쉬',
'조던1 미드 스모크그레이', '조던 1 로우 울프그레이', '조던1 하이 스모크그레이', '조던1 미드 그레이포그',
'조던 1 로우 트레비스 스캇', '조던1 미드 울프그레이'],
dtype='object')
fig , ax = plt.subplots(figsize = (13,10))
wedgeprops={'width': 0.7, 'edgecolor': 'w', 'linewidth': 5}
colors = ["skyblue","red","orange","brown","gray","silver","lightgray", "gray", "green","gray"]
ax.pie(x = df_j["product_name"].value_counts(), labels = df_j_value.index, autopct='%.1f%%'
, startangle=260, counterclock=False, wedgeprops=wedgeprops, textprops ={'size' :15}, colors = colors)
# autopct는 부채꼴 안에 표시될 숫자의 형식을 지정합니다. 소수점 한자리까지 표시하도록 설정했습니다.
# startangle는 부채꼴이 그려지는 시작 각도를 설정합니다.
# counterclock=False로 설정하면 시계 방향 순서로 부채꼴 영역이 표시됩니다
plt.show()
5.1.4 color
df['color'].value_counts()
범고래 2220
그레이 821
하이퍼로얄 618
네이비 399
짐레드 320
스모크그레이 288
다크모카 256
스타피쉬 253
울프그레이 245
라이트본 206
유니버시티블루 188
골든로드 181
코스트 159
바시티그린 156
그레이포그 116
오렌지 116
트레비스 스캇 114
화이트실버 92
블랙 77
Name: color, dtype: int64
plt.figure(figsize=(16,4))
sns.countplot(df['color'])
plt.xticks(rotation=60)
plt.ylim(0,3000)
plt.show()
plt.figure(figsize=(16,4))
sns.countplot(df['color'])
plt.xticks(rotation=60)
plt.ylim(0,1000)
plt.show()
5.1.5 size
plt.figure(figsize=(16,4))
sns.countplot(df['size'])
plt.xticks(rotation=60)
plt.show()
# 남성 사이즈가 많은것 같다
test_df = pd.pivot_table(df, index='size', values='price', aggfunc='median').sort_index()
plt.figure(figsize = (12,4))
sns.barplot(data=test_df, x=test_df.index, y='price')
# size별로 가격 분포를 살펴보았다. 브랜드별로 가능할듯. 일단 전체만
<AxesSubplot:xlabel='size', ylabel='price'>
5.1.6 location1, location2
# 없애기
df['location1'].value_counts()
서울특별시 1930
경기도 1705
NUL 771
인천광역시 461
부산광역시 420
대구광역시 232
경상남도 160
대전광역시 156
충청남도 150
광주광역시 144
충청북도 131
전라북도 129
경상북도 113
강원도 108
울산광역시 87
전라남도 53
제주특별자치도 37
세종특별자치시 35
평택 1
연신내역(수도권3호선,6호선 1
서울역(수도권1호선,4호선,경의선,공항철도 1
Name: location1, dtype: int64
df['location1'].value_counts().index
Index(['서울특별시', '경기도', 'NUL', '인천광역시', '부산광역시', '대구광역시', '경상남도', '대전광역시',
'충청남도', '광주광역시', '충청북도', '전라북도', '경상북도', '강원도', '울산광역시', '전라남도',
'제주특별자치도', '세종특별자치시', '평택', '연신내역(수도권3호선,6호선',
'서울역(수도권1호선,4호선,경의선,공항철도'],
dtype='object')
del1 = df[df['location1'] == '서울역(수도권1호선,4호선,경의선,공항철도'].index
del2 = df[df['location1'] == '연신내역(수도권3호선,6호선'].index
df.iloc[del2]
pid | uid | product_name | brand | product_type | color | size | price | release_price | datetime | date | location1 | location2 | status | used | name | abnormal_price | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
5799 | 134730501 | 1291948 | 뉴발란스 993 그레이 | 뉴발란스 | 뉴발란스 993 | 그레이 | 260 | 469000 | 182700 | 2021-08-01 17:08:00 | 2021-08-01 | 연신내역(수도권3호선,6호선 | 연신내역(수도권3호선,6호선 | 판매중 | 새상품 | [정품/새제품/260] 뉴발란스 993 그레이 | False |
df = df.drop(del1).reset_index(drop=True)
df = df.drop(del2).reset_index(drop=True)
print(df.shape)
df.tail(1)
(6823, 17)
pid | uid | product_name | brand | product_type | color | size | price | release_price | datetime | date | location1 | location2 | status | used | name | abnormal_price | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
6822 | 161653134 | 6165763 | 조던 1 로우 트레비스 스캇 | 조던 | 조던 1 로우 | 트레비스 스캇 | 260 | 1800000 | 189000 | 2021-08-13 13:41:38 | 2021-08-13 | 전라남도 | 여수시 | 판매완료 | 중고 | 조던1로우 260 트레비스 스캇 | False |
# 어디에서 많이 팔았나
data_count.sort_values(by='pid', ascending=False).head(10)
pid | |
---|---|
location2 | |
서구 | 197 |
남구 | 178 |
수원시 | 171 |
부천시 | 159 |
송파구 | 157 |
북구 | 147 |
성남시 | 142 |
중구 | 139 |
고양시 | 137 |
강남구 | 128 |
5.1.7 status
df['status'].value_counts()
판매완료 4483
판매중 2033
예약완료 307
Name: status, dtype: int64
sns.countplot(df['status'])
<AxesSubplot:xlabel='status', ylabel='count'>
sns.countplot(data=df, x='brand', hue='status')
# 판매완료가 제일 많다, 전체 데이터를 크롤링 했기 떄문인 것 같다.
<AxesSubplot:xlabel='brand', ylabel='count'>
plt.figure(figsize=(10,4))
sns.countplot(data=df, x='product_type', hue='status')
# 제품별로도 봤더니 똑같다
<AxesSubplot:xlabel='product_type', ylabel='count'>
5.1.8 used
- 판매자가 정확히 구분하여 올리는 것 같지 않다.
df['used'].value_counts()
중고 3803
새상품 3020
Name: used, dtype: int64
# used 시각화 간단히
sns.barplot(data=df['used'].value_counts(), x=df['used'].value_counts().index, y=df['used'].value_counts().values)
<AxesSubplot:xlabel='used'>
5.1.9 uid
# 누가 가장 상품을 많이 올렸나
pd.pivot_table(df, index='uid', values='pid', aggfunc='count').sort_values(by='pid', ascending=False).head()
pid | |
---|---|
uid | |
7623840 | 57 |
9870858 | 20 |
7930580 | 18 |
4926683 | 15 |
10405026 | 14 |
# 7623840 유저가 제일 많이 상품 올림
df[df['uid'] == 7623840]['product_name'].value_counts()
나이키 덩크로우 코스트 13
나이키 덩크로우 범고래 13
조던1 미드 짐레드 6
뉴발란스 992 화이트실버 5
나이키 덩크하이 범고래 4
뉴발란스 992 네이비 4
조던1 하이 다크모카 3
조던1 미드 울프그레이 2
조던1 미드 그레이포그 2
나이키 덩크로우 유니버시티블루 2
나이키 덩크하이 오렌지 1
조던1 하이 하이퍼로얄 1
뉴발란스 992 그레이 1
Name: product_name, dtype: int64
# 7623840 유저는 상품은 많이 올렸는데 판매완료가 훨씬 적다
df[df['uid'] == 7623840]['status'].value_counts()
판매중 44
판매완료 13
Name: status, dtype: int64
# 7623840 유저는 군포사람, 예전 상품도 안팔린게 많음. 시세 반영을 다시 안하는듯.
df[(df['uid'] == 7623840) & (df['status'] == '판매중')].sort_values(by='date')
5.2 발매가와 현물가 비교 분석
- 브랜드별로 살펴본다.
5.2.1 나이키
# 제품별 df 프레임 구축
df_n = df[df["brand"] == "나이키"]
df_n.groupby("product_name")["price"].median()
product_name
나이키 덩크로우 골든로드 156000
나이키 덩크로우 라이트본 260000
나이키 덩크로우 바시티그린 300000
나이키 덩크로우 범고래 310000
나이키 덩크로우 유니버시티블루 250000
나이키 덩크로우 코스트 310000
나이키 덩크하이 네이비 198000
나이키 덩크하이 범고래 250000
나이키 덩크하이 오렌지 175000
Name: price, dtype: int64
df_n.groupby("product_name")["release_price"].median()
product_name
나이키 덩크로우 골든로드 119000
나이키 덩크로우 라이트본 119000
나이키 덩크로우 바시티그린 119000
나이키 덩크로우 범고래 119000
나이키 덩크로우 유니버시티블루 119000
나이키 덩크로우 코스트 119000
나이키 덩크하이 네이비 129000
나이키 덩크하이 범고래 129000
나이키 덩크하이 오렌지 129000
Name: release_price, dtype: int64
# 현물가
fig , ax = plt.subplots()
ax.barh(df_n.groupby("product_name")["price"].median().sort_values(ascending =True).index,
df_n.groupby("product_name")["price"].median().sort_values(ascending =True).values, color ="black")
plt.show()
# 발매가
fig , ax = plt.subplots()
ax.barh(df_n.groupby("product_name")["release_price"].median().sort_values(ascending =True).index,
df_n.groupby("product_name")["release_price"].median().sort_values(ascending = True).values, color ="black")
<BarContainer object of 9 artists>
# 새상품 중고 상품 나누기
df_n_new = df_n[df_n["used"]== '' ]
df_n_used = df_n[df_n["used"]== 1 ]
# 현물가와 발매가와의 차이 : 어느 제품이 더 올랐나.
fig , ax = plt.subplots(figsize = (10,8))
ax.barh(df_n.groupby("product_name")["price"].median().index,
df_n.groupby("product_name")["price"].median().values, label ="중고시장 가격", color ="black")
ax.barh(df_n.groupby("product_name")["release_price"].median().index,
df_n.groupby("product_name")["release_price"].median().values, label ="발매가", color = "gray")
plt.legend()
plt.show()
sub = df_n.groupby("product_name")["price"].median() - df_n.groupby("product_name")["release_price"].median()
df_n_new_sub = df_n_new.groupby("product_name")["price"].median() - df_n.groupby("product_name")["release_price"].median()
df_n_used_sub = df_n_used.groupby("product_name")["price"].median() - df_n.groupby("product_name")["release_price"].median()
fig , ax = plt.subplots(figsize = (10,8))
ax.barh(sub.index,sub.values,color ="black")
ax.set_title("발매가 대비 중고시장 현물가 가격 상승 추이 ")
plt.show()
df_n_new = df_n[df_n["used"]== '새상품' ]
df_n_used = df_n[df_n["used"]== '중고' ]
df_n_new_sub = df_n_new.groupby("product_name")["price"].median() - df_n.groupby("product_name")["release_price"].median()
df_n_used_sub = df_n_used.groupby("product_name")["price"].median() - df_n.groupby("product_name")["release_price"].median()
# 안나옴
fig , axes = plt.subplots(2,1, figsize = (10,8))
axes[0].barh(df_n_new_sub.index,
df_n_new_sub.values, label =" 새상품 ", color ="black")
axes[1].barh(df_n_used_sub.index,
df_n_used_sub.values, label ="중고상품", color = "gray")
axes[1].set_xlim(0,250000)
plt.legend()
plt.show()
# 확실히 중고가격이 더 낮은것으로 확인되었다. 이는 가격변수에 영향을 미칠 것으로 판단된다.
df_n_value = df_n["product_name"].value_counts()
df_n_value.index
Index(['나이키 덩크로우 범고래', '나이키 덩크하이 범고래', '나이키 덩크로우 라이트본', '나이키 덩크로우 유니버시티블루',
'나이키 덩크로우 골든로드', '나이키 덩크하이 네이비', '나이키 덩크로우 코스트', '나이키 덩크로우 바시티그린',
'나이키 덩크하이 오렌지'],
dtype='object')
df_n_new.value = df_n_new["product_name"].value_counts()
df_n_used.value = df_n_used["product_name"].value_counts()
5.2.2 조던¶
df_j = df[df["brand"]== "조던"]
df_j.groupby("product_name")["price"].median().sort_values(ascending =False)
product_name
조던 1 로우 트레비스 스캇 1630000
조던1 하이 다크모카 499500
조던1 미드 스모크그레이 380000
조던 1 로우 스타피쉬 310000
조던1 미드 그레이포그 290000
조던1 하이 하이퍼로얄 290000
조던 1 로우 울프그레이 284000
조던1 미드 울프그레이 280000
조던1 하이 스모크그레이 260000
조던1 미드 짐레드 170000
Name: price, dtype: int64
df_j.groupby("product_name")["release_price"].median().sort_values(ascending =False)
product_name
조던1 하이 다크모카 199000
조던1 하이 스모크그레이 199000
조던1 하이 하이퍼로얄 199000
조던 1 로우 트레비스 스캇 189000
조던 1 로우 스타피쉬 159000
조던1 미드 그레이포그 139000
조던1 미드 스모크그레이 139000
조던1 미드 울프그레이 139000
조던1 미드 짐레드 139000
조던 1 로우 울프그레이 119000
Name: release_price, dtype: int64
fig , ax = plt.subplots()
ax.barh(df_j.groupby("product_name")["price"].median().index,
df_j.groupby("product_name")["price"].median().values, color ="black")
plt.show()
fig , ax = plt.subplots(figsize = (10,8))
ax.barh(df_j.groupby("product_name")["price"].median().index,
df_j.groupby("product_name")["price"].median().values, label ="중고시장 가격", color ="black")
ax.barh(df_j.groupby("product_name")["release_price"].median().index,
df_j.groupby("product_name")["release_price"].median().values, label ="발매가", color = "gray")
plt.legend()
plt.show()
sub_j = df_j.groupby("product_name")["price"].median().sort_values(ascending =False) - df_j.groupby("product_name")["release_price"].median().sort_values(ascending =False)
# 새상품 중고 상품 나누기
df_j_new = df_j[df_j["used"]== '새상품' ]
df_j_used = df_j[df_j["used"]== '중고' ]
df_j_new_sub = df_j_new.groupby("product_name")["price"].median() - df_j.groupby("product_name")["release_price"].median()
df_j_used_sub = df_j_used.groupby("product_name")["price"].median() - df_j.groupby("product_name")["release_price"].median()
fig , axes = plt.subplots(2,1, figsize = (10,8))
axes[0].barh(df_j_new_sub.index,
df_j_new_sub.values, label =" 새상품 ", color ="black")
axes[1].barh(df_j_used_sub.index,
df_j_used_sub.values, label ="중고상품", color = "gray")
plt.legend()
plt.show()
df_j_new_value = df_j_new["product_name"].value_counts()
df_j_used_value = df_j_used["product_name"].value_counts()
5.2.3 뉴발란스
df_b = df[df["brand"]== "뉴발란스"]
df_b.groupby("product_name")["price"].median().sort_values(ascending =False)
product_name
뉴발란스 992 그레이 340000
뉴발란스 992 화이트실버 302500
뉴발란스 992 네이비 270000
뉴발란스 993 그레이 240000
뉴발란스 993 네이비 150000
뉴발란스 993 블랙 150000
Name: price, dtype: int64
df_b.groupby("product_name")["release_price"].median().sort_values(ascending =False)
product_name
뉴발란스 992 그레이 259000
뉴발란스 992 네이비 259000
뉴발란스 992 화이트실버 259000
뉴발란스 993 그레이 182700
뉴발란스 993 네이비 182700
뉴발란스 993 블랙 182700
Name: release_price, dtype: int64
sub_b = df_b.groupby("product_name")["price"].median()- df_b.groupby("product_name")["release_price"].median()
fig , ax = plt.subplots()
ax.barh(df_b.groupby("product_name")["price"].median().index,
df_b.groupby("product_name")["price"].median().values, color ="black")
plt.show()
fig , ax = plt.subplots(figsize = (10,8))
ax.barh(df_b.groupby("product_name")["price"].median().index,
df_b.groupby("product_name")["price"].median().values, label ="중고시장 가격", color ="black")
ax.barh(df_b.groupby("product_name")["release_price"].median().index,
df_b.groupby("product_name")["release_price"].median().values, label ="발매가", color ="gray")
plt.legend()
plt.show()
df_b_new = df_b[df_b["used"]== '새상품' ]
df_b_used = df_b[df_b["used"]== '중고' ]
df_b_new_value = df_b_new["product_name"].value_counts()
df_b_used_value = df_b_used["product_name"].value_counts()
display(df_b_used.groupby("product_name")["price"].median().sort_values(ascending =False),df_b_new.groupby("product_name")["price"].median().sort_values(ascending =False))
product_name
뉴발란스 992 그레이 320000
뉴발란스 992 화이트실버 280000
뉴발란스 992 네이비 250000
뉴발란스 993 그레이 200000
뉴발란스 993 블랙 149500
뉴발란스 993 네이비 129000
Name: price, dtype: int64
product_name
뉴발란스 992 그레이 358000
뉴발란스 992 네이비 315000
뉴발란스 992 화이트실버 310000
뉴발란스 993 그레이 260000
뉴발란스 993 네이비 220000
뉴발란스 993 블랙 220000
Name: price, dtype: int64
df_b_new_sub = df_b_new.groupby("product_name")["price"].median() - df_b.groupby("product_name")["release_price"].median()
df_b_used_sub = df_b_used.groupby("product_name")["price"].median() - df_b.groupby("product_name")["release_price"].median()
fig , ax = plt.subplots(figsize = (10,8))
ax.bar(df_b_used_sub.index, df_b_used_sub.values, color ="black")
plt.xticks(rotation =45)
fig.tight_layout()
plt.show()
# 발매가보다 더 떨어지는 경우도 있다. 이는 조던과 나이키와는 다른 양상.
# 나이키, 조던은 중고제품임에도 발매가보다 높았는데, 뉴발의 중고제품은 역시 가격이 떨어지는 것으로 보였다.
# 하지만, 뉴발 992 그레이는 리셀 대상 상품인 것 같다.
<Figure size 1440x576 with 0 Axes>
pivot = pd.pivot_table(df, index='brand', values='price', aggfunc='mean')
pivot.reset_index()
brand | price | |
---|---|---|
0 | 나이키 | 282664 |
1 | 뉴발란스 | 290999 |
2 | 조던 | 301027 |
sns.barplot(data=pivot.reset_index(), x='brand', y='price')
plt.ylim([250000,320000])
# 뉴발이 발매가가 제일 높음에도 불구하고, 조던이 더 높은 가격대에 형성되어있습니다.
(250000.0, 320000.0)
# 거래량의 변화
plt.figure(figsize=(20,8))
for col in column_list:
sns.lineplot(data=pivot_df['count'], x=pivot_df['count'].index, y=pivot_df['count'][col], label=col)
plt.title('브랜드별 번개장터 거래량의 변화')
plt.xlabel('date')
plt.ylabel('count')
plt.ylim([0,100])
# plt.xlim(pd.to_datetime(['2021-05-11', '2021-11-11']))
plt.legend()
plt.show()
# 번개장터에서 판매완료된 데이터는 일정기간 지난 후에 삭제할 가능성이 존재한다. 그래서 최근의 거래량이 더 많이 나온 것 같다.
5.5 네이버 트렌드 검색량 추이
- 브랜드별로 제품의 검색량 추이가 어땠는지 살펴본다.
- 출처 :
- 기준 :
shop = pd.read_csv('./data/NAVER_shop.csv')
trend = pd.read_csv('./data/NAVER_trend.csv')
trend.head(1)
날짜 | 조던1 하이 다크모카 | 조던1 하이 스모크그레이 | 조던1 하이 하이퍼로얄 | 조던1 미드 그레이포크 | 조던1 미드 스모크그레이 | 조던1 미드 울프그레이 | 조던1 미드 짐레드 | 조던1 로우 스타피쉬 | 조던1 로우 울프그레이 | 조던1 로우 트레비스 스캇 | 나이키 덩크하이 네이비 | 나이키 덩크하이 범고래 | 나이키 덩크하이 오렌지 | 나이키 덩크 로우 골든로드 | 나이키 덩크 로우 라이트본 | 나이키 덩크 로우 바시티그린 | 나이키 덩크 로우 범고래 | 나이키 덩크 로우 유니버시티블루 | 나이키 덩크로우 코스트 | 뉴발란스 992 그레이 | 뉴발란스 992 네이비 | 뉴발란스 992 블랙그레이 | 뉴발란스 992 화이트실버 | 뉴발란스 993 그레이 | 뉴발란스 993 네이비 | 뉴발란스 993 블랙 | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 2020-11-01 | 0 | 7 | 0 | 0 | 3 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 7 | 5 | 0 | 1 | 32 | 7 | 15 |
trend['날짜'] = pd.to_datetime(trend['날짜'])
trend.columns[1:]
Index(['조던1 하이 다크모카', '조던1 하이 스모크그레이', '조던1 하이 하이퍼로얄', '조던1 미드 그레이포크',
'조던1 미드 스모크그레이', '조던1 미드 울프그레이', '조던1 미드 짐레드', '조던1 로우 스타피쉬',
'조던1 로우 울프그레이', '조던1 로우 트레비스 스캇', '나이키 덩크하이 네이비', '나이키 덩크하이 범고래',
'나이키 덩크하이 오렌지', '나이키 덩크 로우 골든로드 ', '나이키 덩크 로우 라이트본', '나이키 덩크 로우 바시티그린',
'나이키 덩크 로우 범고래', '나이키 덩크 로우 유니버시티블루', '나이키 덩크로우 코스트', '뉴발란스 992 그레이',
'뉴발란스 992 네이비', '뉴발란스 992 블랙그레이', '뉴발란스 992 화이트실버', '뉴발란스 993 그레이',
'뉴발란스 993 네이비', '뉴발란스 993 블랙'],
dtype='object')
jodan1_cols = trend.columns[trend.columns.str.contains('조던1')]
nike_cols = trend.columns[trend.columns.str.contains('나이키')]
newbal_cols = trend.columns[trend.columns.str.contains('뉴발란스')]
plt.figure(figsize=(20,8))
for col in jodan1_cols:
sns.lineplot(data=trend, x=trend['날짜'], y=trend[col], label=col)
plt.title('조던1 검색량 추이')
plt.xlabel('date')
plt.ylabel('count')
# plt.ylim([0,50])
# plt.xlim(pd.to_datetime(['2021-05-11', '2021-11-11']))
plt.legend()
plt.show()
# 발매일을 유추할 수 잇다.
# 실제로 찾아봤고, 예를들어, 조던1 로우 스타피쉬 드로우일정이 8/30이었고, 실제로 검색량이 늘어남을 볼 수 있다.
# 조던1 하이 다크모카 11/13 드로우
# 이를 바탕으로 가격변화에서 발매일이 요인으로 작용할 수 있는지 확인해볼 수 있다.
# https://footsell.com/g2/bbs/board.php?bo_table=jordannews&wr_id=576401&sca=&sfl=wr_subject%7C%7Cwr_content&stx=%EC%A1%B0%EB%8D%98+%EB%B8%94%EB%9E%99&sop=and&page=3&scrap_mode=
test_df = data[data['product_name'] == '조던 1 로우 스타피쉬']
print(test_df.shape)
test_df.tail(1)
(253, 17)
pid | uid | product_name | brand | product_type | color | size | price | release_price | datetime | date | location1 | location2 | status | used | name | abnormal_price | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
6710 | 163131611 | 74872991 | 조던 1 로우 스타피쉬 | 조던 | 조던 1 로우 | 스타피쉬 | 0 | 300000 | 159000 | 2021-08-30 10:21:58 | 2021-08-30 | 인천광역시 | 부평구 | 판매완료 | 중고 | 조던 로우 1 스타피쉬 새상품 | False |
# 원하는 분류 columns에 넣기 (brand/product_type/color/used)
# 해당날짜에 판매기록이 없으면 NaN으로 표시됨.
# aggfunc에서 median(중앙값-이상치에 덜 영향), mean(평균), count(몇개인지 보기위함)
pivot_df = pd.pivot_table(test_df, index='date', values='price', aggfunc=['median','mean','count']).sort_index()
print(pivot_df.shape)
pivot_df.head()
(60, 3)
median | mean | count | |
---|---|---|---|
price | price | price | |
date | |||
2021-08-30 | 300000 | 299158 | 19 |
2021-08-31 | 300000 | 296818 | 11 |
2021-09-01 | 290000 | 292727 | 11 |
2021-09-02 | 290000 | 291538 | 13 |
2021-09-03 | 300000 | 307692 | 13 |
plt.figure(figsize=(20,8))
sns.lineplot(data=pivot_df['mean'], x=pivot_df['mean'].index, y=pivot_df['mean']['price'])
<AxesSubplot:xlabel='date', ylabel='price'>
plt.figure(figsize=(20,8))
for col in nike_cols:
sns.lineplot(data=trend, x=trend['날짜'], y=trend[col], label=col)
plt.title('나이키 덩크 검색량 추이')
plt.xlabel('date')
plt.ylabel('count')
# plt.ylim([0,50])
# plt.xlim(pd.to_datetime(['2021-05-11', '2021-11-11']))
plt.legend()
plt.show()
plt.figure(figsize=(20,8))
for col in newbal_cols:
sns.lineplot(data=trend, x=trend['날짜'], y=trend[col], label=col)
plt.title('뉴발란스 검색량 추이')
plt.xlabel('date')
plt.ylabel('count')
# plt.ylim([0,50])
# plt.xlim(pd.to_datetime(['2021-05-11', '2021-11-11']))
plt.legend()
plt.show()
# 스케일이 똑같아야한다!!!!
plt.figure(figsize=(20,8))
sns.lineplot(data=test, x='date', y='mean', label='price_mean')
sns.lineplot(data=test, x='date', y='trend_count', label='trend_count')
plt.title('나이키 덩크로우 범고래 가격과 검색량 추이 비교')
plt.xlabel('date')
plt.ylabel('count')
# plt.ylim([0,50])
# plt.xlim(pd.to_datetime(['2021-05-11', '2021-11-11']))
plt.legend()
plt.show()
# 제일 많은 표본의 나이키 덩크로우 범고래를 대상으로 검색량 추이와 가격시세 변화를 비교해보았다.
# 범고래는 지속적으로 인기가 있었던 제품이기에 드로우에 큰 영향을 받지는 않은 것으로 보인다.
# 다만 번개장터라는 표본이 모든 제품을 대표하는 것은 아니기에 유의미한 분석을 하기에는 한계가 있었다.
6. Conclusion
6.1 분석 결과
- 이번 EDA를 통해서 어떤 요인들이 시세에 영향을 미쳤는지 알게되었고 이를 바탕으로 모델을 개발하면 신발 구매 타이밍을 결정하거나 새로 출시되는 신발의 시세 예측을 해볼 수도 있을 것 같다. (물론 그때는 디자인적인요소에 대한 지표도 만들어서 추가해야하겠지만)
- 가격이 추세에 맞게 책정되는 리셀사이트들과 다르게 중고사이트는 개인 판매자들이 임의적으로 가격을 정해 판매하는 시스템으로 일반적으로 설정된 가격과 상이할 수 있다는 점이 보였습니다.
6.2 아쉬운 점 & 느낀 점
아쉬움 점
- 가격변화에 영향을 미치는 변수들의 데이터를 얻지 못한점
- 검색어 트렌드의 변화와 중고시장 리셀제품 간의 상관관계를 기대했지만, 유의미한 상관관계는 얻지 못했다.
- 개인이 셀링하는 중고플랫폼의 특성상 완전한 변인의 통제가 어려웠던 점 (가격 이상치, 허위매물 등)
- 데이터가 한정되어있어서 하려는 의도를 제대로 하지 못한 점도 아쉽다.
- 프로젝트할 산업군에 대해서 도메인 지식이 있다면 분석에 도움이 많이 될 것 같다.
6.3 이후의 프로젝트
- 발매일, 재발매일, 드로우일정에 대한 컬럼을 새로 생성하면 가격에 영향을 주는 요인으로 사용할 수 있을 것 같다 (네이버 트렌드와 비슷하게)
- 인스타에서 크롤링
- 디자인적인요소에 대한 지표를 컬럼으로 설정하여 제품의 디자인적 특성도 요인으로 사용할 수 있을 것 같다
- 신발의 가격에 영향을 미치는 요인에 대한 분석을 좀 더 면밀히 수행하고, 신발의 가격을 예측할 수 있는 모델을 개발한다면,
- 신발 구매 타이밍을 결정하거나, 새로 출시되는 신발의 시세 예측을 해볼 수도 있을 것 같다.
- 당근마켓 등 모든 중고시장의 데이터를 가져와서 신발 데이터를 모두 수집하여 서비스 제공가능할 것 같다.
- 중고시장 전체의 리셀신발 시세를 중고/새상품에 따라 분류를 해줄 수도 있고,
- 활용방향이 무궁무진하다.
'비즈니스 분석가 양성과정' 카테고리의 다른 글
[22일차] BDA과정 Business Analyst를 위한 핵심 SQL 실전 (0) | 2021.11.17 |
---|---|
[21일차] BDA과정 데이터베이스와 SQL (0) | 2021.11.16 |
[15일차] BDA과정 - 동적페이지 크롤링 (0) | 2021.11.08 |
[14일차] BDA과정 - Python Crawler(파이썬 크롤링) (0) | 2021.11.05 |
[12일 ,13일] BDA 과정 - 탐색적분석 (0) | 2021.11.04 |