Alroha

[Olist 이커머스 분석 #2] 데이터 EDA- 주문·매출·배송·리뷰 탐색 본문

IT/데이터분석

[Olist 이커머스 분석 #2] 데이터 EDA- 주문·매출·배송·리뷰 탐색

헤이로하 2026. 6. 24. 16:58

🔙 이전 포스팅

2026.06.23 - [IT/데이터분석] - [Olist 이커머스 분석 #1] MySQL에 데이터 적재


이번 글에서는 Olist 9개 테이블의 구조를 파악하고, 주문·매출·배송·리뷰 4가지 축으로 데이터를 탐색한 과정을 정리했습니다.

🛠️ 사용 도구: MySQL 8.x + MySQL Workbench 📁 데이터: Olist Brazilian E-Commerce (9개 테이블, 약 10만 주문)


📐 ERD — 테이블 연결 구조

customers ─── orders ─── order_items ─── products
                │               └──────── sellers
                ├── order_payments
                └── order_reviews

geolocation ← customers.zip / sellers.zip
category_translation ← products.product_category_name

핵심 키 연결 테이블

order_id orders, order_items, order_payments, order_reviews
customer_id customers, orders
product_id products, order_items
seller_id sellers, order_items

💡 : 분석 전 ERD를 손으로 한 번 그려보면 JOIN 실수가 줄어듦


🔍 STEP 1. 주문 현황 파악

전체 주문 수 & 데이터 기간

SELECT
    COUNT(*) AS total_orders,
    MIN(order_purchase_timestamp) AS first_order,
    MAX(order_purchase_timestamp) AS last_order
FROM orders;

주문 상태별 분포

SELECT
    order_status,
    COUNT(*) AS cnt,
    ROUND(COUNT(*) * 100.0 / SUM(COUNT(*)) OVER(), 1) AS pct
FROM orders
GROUP BY order_status
ORDER BY cnt DESC;

월별 주문 추이

SELECT
    DATE_FORMAT(order_purchase_timestamp, '%Y-%m') AS order_month,
    COUNT(*) AS order_cnt
FROM orders
WHERE order_status != 'canceled'
GROUP BY order_month
ORDER BY order_month;

📊 발견: 2017년 하반기부터 주문이 급증하고, 2018년 초 피크를 찍는 성장 패턴 확인


💰 STEP 2. 매출 파악

전체 매출 & 평균 주문금액

SELECT
    ROUND(SUM(payment_value), 0)  AS total_revenue,
    ROUND(AVG(payment_value), 2)  AS avg_order_value,
    ROUND(MAX(payment_value), 2)  AS max_order_value
FROM order_payments
WHERE payment_type != 'not_defined';

카테고리별 매출 TOP 10

SELECT
    COALESCE(t.product_category_name_english, p.product_category_name, 'Unknown') AS category,
    COUNT(DISTINCT oi.order_id)  AS order_cnt,
    ROUND(SUM(oi.price), 0)     AS revenue
FROM order_items oi
JOIN products p ON oi.product_id = p.product_id
LEFT JOIN category_translation t ON p.product_category_name = t.product_category_name
GROUP BY category
ORDER BY revenue DESC
LIMIT 10;

결제수단별 비중

SELECT
    payment_type,
    COUNT(*) AS cnt,
    ROUND(COUNT(*) * 100.0 / SUM(COUNT(*)) OVER(), 1) AS pct
FROM order_payments
GROUP BY payment_type
ORDER BY cnt DESC;

📊 발견: 신용카드(credit_card)가 압도적 1위, boleto(브라질 전통 지불방식)가 2위


🚚 STEP 3. 배송 소요일 분석

실제 배송일 vs 예상 배송일

SELECT
    ROUND(AVG(DATEDIFF(order_delivered_customer_date, order_purchase_timestamp)), 1) AS avg_actual_days,
    ROUND(AVG(DATEDIFF(order_estimated_delivery_date, order_purchase_timestamp)), 1) AS avg_estimated_days
FROM orders
WHERE order_delivered_customer_date IS NOT NULL;

배송 소요일 구간별 분포

SELECT
    CASE
        WHEN DATEDIFF(order_delivered_customer_date, order_purchase_timestamp) <= 7  THEN '7일 이내'
        WHEN DATEDIFF(order_delivered_customer_date, order_purchase_timestamp) <= 14 THEN '8~14일'
        WHEN DATEDIFF(order_delivered_customer_date, order_purchase_timestamp) <= 21 THEN '15~21일'
        ELSE '22일 이상'
    END AS delivery_range,
    COUNT(*) AS cnt
FROM orders
WHERE order_delivered_customer_date IS NOT NULL
GROUP BY delivery_range
ORDER BY MIN(DATEDIFF(order_delivered_customer_date, order_purchase_timestamp));

📊 발견: 브라질은 국토가 넓어 평균 배송이 12일 이상 — 한국 익일배송과 전혀 다른 시장


⭐ STEP 4. 리뷰 분석

리뷰 점수 분포

SELECT
    review_score,
    COUNT(*) AS cnt,
    ROUND(COUNT(*) * 100.0 / SUM(COUNT(*)) OVER(), 1) AS pct
FROM order_reviews
GROUP BY review_score
ORDER BY review_score;

배송 지연 여부 vs 리뷰 점수

SELECT
    CASE
        WHEN order_delivered_customer_date > order_estimated_delivery_date THEN '지연'
        ELSE '정시/조기'
    END AS delivery_status,
    ROUND(AVG(r.review_score), 2) AS avg_review_score,
    COUNT(*) AS cnt
FROM orders o
JOIN order_reviews r ON o.order_id = r.order_id
WHERE o.order_delivered_customer_date IS NOT NULL
GROUP BY delivery_status;

📊 발견: 배송 지연 시 리뷰 점수가 유의미하게 낮아짐 → 배송 SLA가 고객 만족의 핵심 지표


🩺 STEP 5. 데이터 품질 체크

분석 지표를 설계하기 전, 반드시 결측치·이상치를 확인해야 합니다.

orders 테이블 NULL 현황

SELECT
    COUNT(*) AS total,
    SUM(CASE WHEN order_approved_at IS NULL THEN 1 ELSE 0 END)             AS null_approved,
    SUM(CASE WHEN order_delivered_carrier_date IS NULL THEN 1 ELSE 0 END)  AS null_carrier,
    SUM(CASE WHEN order_delivered_customer_date IS NULL THEN 1 ELSE 0 END) AS null_delivered
FROM orders;

products 테이블 결측치 현황

SELECT
    COUNT(*) AS total,
    SUM(CASE WHEN product_category_name IS NULL THEN 1 ELSE 0 END) AS null_category,
    SUM(CASE WHEN product_weight_g = 0 OR product_weight_g IS NULL THEN 1 ELSE 0 END) AS zero_weight
FROM products;

이상치 탐지 — 비정상 고액 주문

SELECT
    order_id,
    payment_value
FROM order_payments
WHERE payment_value > 5000
ORDER BY payment_value DESC
LIMIT 10;

📊 발견: order_delivered_customer_date NULL은 배송 미완료 주문(정상). 반면 product_weight_g = 0은 실제 결측값으로 배송비 계산 시 제외 필요


🚨 시행착오 & 트러블슈팅

❌ 오류. Unknown column 오타

Error Code: 1054. Unknown column 'payment_vlaue' in 'field list'

원인: payment_value를 payment_vlaue로 오타 해결: 컬럼명 재확인 후 수정 교훈: SQL은 오타에 관대하지 않습니다. 자동완성(Ctrl+Space)을 적극 활용하기!


💡 회고

잘한 점

  • ERD를 먼저 파악하고 쿼리를 작성하니 JOIN 방향을 헷갈리지 않았음
  • NULL을 "오류"가 아니라 "비즈니스 상태"로 해석하는 시각이 생김 (미배송 = NULL은 정상)
  • 배송 지연과 리뷰 점수의 관계를 EDA 단계에서 미리 발견 → 4강 데이터마트 설계 방향 결정에 활용

아쉬운 점

  • 컬럼명 오타로 Error 1054 발생 → Workbench 자동완성 습관 필요
  • OVER() 윈도우 함수가 처음에는 낯설었음 → 비율 계산에 매우 유용하므로 반복 연습 필요

배운 것

"NULL은 적이 아니다. NULL이 왜 생겼는지를 이해하는 것이 분석의 시작이다." order_delivered_customer_date가 NULL인 건 배송 중이거나 취소된 주문 — 이걸 모르고 AVG 돌리면 배송 소요일이 왜곡됩니다.


[과제] 

아래 5개 질문에 직접 쿼리를 작성하고 숫자를 찾아보세요.

  • 가장 주문이 많은 요일은? 월요일

  • 신용카드 할부 평균 개월 수는? 4개월

  • 리뷰 점수 1점 주문 중 배송 지연 비율은? 68.9

 

  • 판매자 수가 가장 많은 주(state)는? SP

  • 평균 상품 무게는 몇 g인가? 2276.3g

 

🔜 다음 포스팅

[Olist 이커머스 분석 #3] SQL로 분석용 데이터마트 만들기 — RFM · 배송 · 리뷰 마트 3종


본 시리즈는 Kaggle의 Olist Brazilian E-Commerce 데이터셋을 활용한 포트폴리오 프로젝트입니다.