Post

数据分析案例:Olist 巴西电商公共数据集

数据分析案例:Olist 巴西电商公共数据集

数据集来源

这是真实的商业数据,所有商店和合作伙伴的文本都已替换为《权力的游戏》中的家族名称。

了解 Olist 商业模式

Olist 是一个 B2B2C 的平台,为长尾中小商家提供将 SKU 一站分发到各大电商平台的服务,为他们提供在各大电商平台的信用背书,降低中小商家入驻大平台的门槛。对于大平台来说,Olist 是一个超级大卖家,依托众多中小商家,提供了海量 SKU。 Olist 的利润主要来自 SaaS 软件的订阅费(月费/年费)和每笔订单的佣金抽成。

上述只针对 2016~2018 年间的 Olist,现在的 Olist 的业务版图已经扩展了非常多。

选定北极星指标

任何商业公司的最终目的都是盈利,所以个人思路是:从利润开始层层拆分,每层只拆分成长性更高的那个指标

  • 第一次拆分:$利润 = 营业额 \times 利润率$
  • 第二次拆分:$营业额 = 订阅费收入 + 佣金收入$
  • 第三次拆分:$佣金收入 = 总交易额(GMV) \times 佣金率$

再往下拆会不得不产生信息损失,导致决策信息失真,所以这里选择 GMV 作为北极星指标

理解数据集结构

数据源网站已经给出了所有字段的准确语义和 Data Schema,这里根据已有信息画出 ER 图即可。

erDiagram
	olist_customers {
		string customer_id PK
		string customer_unique_id UK
		string customer_zip_code_prefix
		string customer_city
		string customer_state
	}

	olist_orders {
		string order_id PK
		string customer_id FK
		string order_status
		timestamp order_purchase_timestamp
		timestamp order_approved_at
		timestamp order_delivered_carrier_date
		timestamp order_delivered_customer_date
		timestamp order_estimated_delivery_date
	}

	olist_order_items {
		string order_id  PK, FK
		int order_item_id PK
		string product_id FK
		string seller_id FK
		timestamp shipping_limit_date
		float price
		float freight_value
	}

	olist_products {
		string product_id PK
		string product_category_name 
		int product_name_lenght 
		int product_description_lenght 
		int product_photos_qty 
		float product_weight_g 
		float product_length_cm 
		float product_height_cm 
		float product_width_cm 
	}

	olist_sellers {
		string seller_id PK
		string seller_zip_code_prefix 
		string seller_city 
		string seller_state 
	}

    olist_order_reviews {
		string review_id PK
		string order_id FK
		int review_score 
		string review_comment_title 
		string review_comment_message 
		timestamp review_creation_date 
		timestamp review_answer_timestamp 
	}

	olist_order_payments {
		string order_id FK
		int payment_sequential 
		string payment_type 
		int payment_installments 
		float payment_value 
	}

	olist_geolocation {
		string geolocation_zip_code_prefix 
		float geolocation_lat 
		float geolocation_lng 
		string geolocation_city 
		string geolocation_state 
	}

	product_category_name_translation {
		string product_category_name PK
		string product_category_name_english UK
	}

	olist_marketing_qualified_leads {
		string mql_id PK
		date first_contact_date 
		string landing_page_id 
		string origin 
	}

	olist_closed_deals {
		string mql_id FK
		string seller_id FK
		string sdr_id 
		string sr_id 
		timestamp won_date 
		string business_segment 
		string lead_type 
		string lead_behaviour_profile 
		bool has_company 
		bool has_gtin 
		string average_stock 
		string business_type 
		string declared_product_catalog_size 
		string declared_monthly_revenue 
	}

	olist_customers||--o{olist_orders:"places"
	olist_geolocation||--o{olist_customers:"locates"
	olist_orders||--|{olist_order_items:"contains"
	olist_orders||--|{olist_order_payments:"paid_via"
	olist_orders||--o{olist_order_reviews:"receives"
	olist_products||--o{olist_order_items:"defines"
	olist_sellers||--o{olist_order_items:"fulfills"
	olist_geolocation||--o{olist_sellers:"locates"
	product_category_name_translation||--o{olist_products:"translates_category"
	olist_marketing_qualified_leads||--o|olist_closed_deals:"converts_to"
	olist_closed_deals|o--||olist_sellers:"becomes_seller"

探索性数据分析与问题拆解

从北极星指标入手,同期群多维度观察,发现并层层拆解问题。

单纯的 GMV 只是一个随着时间不断增长的 “虚荣指标”,我们实际需要洞察的是它的(年/月/季度)变化率。

利用 olist_orders_datasetolist_order_items_dataset 计算 GMV 变化率之前首先要审计和清洗数据。

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
------------------------------------
  olist_orders_dataset 审计结果
------------------------------------


                                                                          alerts    type  count  missing  missing_pct  unique  unique_pct                   top  top_cnt   top_pct
column                                                                                                                                                                            
order_id                                             may_pk_or_uk | may_need_nlp  object  99441        0     0.000000   99441    1.000000  00010242fe8c5a6d1ba2        1  0.000010
customer_id                                          may_pk_or_uk | may_need_nlp  object  99441        0     0.000000   99441    1.000000  00012a2ce6f8dcda20d0        1  0.000010
order_status                                                        too_many_top  object  99441        0     0.000000       8    0.000080             delivered    96478  0.970203
order_purchase_timestamp                             may_pk_or_uk | may_need_nlp  object  99441        0     0.000000   98875    0.994308   2017-11-20 10:59:08        3  0.000030
order_approved_at                             missing_need_handle | may_need_nlp  object  99441      160     0.001609   90733    0.912430   2018-02-27 04:31:10        9  0.000091
order_delivered_carrier_date                                 missing_need_handle  object  99441     1783     0.017930   81018    0.814734   2018-05-09 15:48:00       47  0.000473
order_delivered_customer_date  missing_need_handle | may_pk_or_uk | may_need_nlp  object  99441     2965     0.029817   95664    0.962018   2016-10-27 17:32:07        3  0.000030
order_estimated_delivery_date                                                     object  99441        0     0.000000     459    0.004616   2017-12-20 00:00:00      522  0.005249

------------------------------------
  olist_order_items_dataset 审计结果
------------------------------------

                                                              alerts     type   count  missing  missing_pct  unique  unique_pct                   top  top_cnt   top_pct   min      max        mean  median         std   zero  zero_pct  negative  negative_pct  skewness    kurtosis
column                                                                                                                                                                                                                                                                                
order_id                                                               object  112650        0          0.0   98666    0.875863  8272b63d03f5f79c56e9     21.0  0.000186   NaN      NaN         NaN     NaN         NaN    NaN       NaN       NaN           NaN       NaN         NaN
order_item_id        check_polar_valid | high_skewed | may_high_kurt    int64  112650        0          0.0      21    0.000186                   NaN      NaN       NaN  1.00    21.00    1.197834    1.00    0.705124    0.0    0.0000       0.0           0.0  7.580356  103.857361
product_id                                                             object  112650        0          0.0   32951    0.292508  aca2eb7d00ea1a7b8ebd    527.0  0.004678   NaN      NaN         NaN     NaN         NaN    NaN       NaN       NaN           NaN       NaN         NaN
seller_id                                                              object  112650        0          0.0    3095    0.027474  6560211a19b47992c366   2033.0  0.018047   NaN      NaN         NaN     NaN         NaN    NaN       NaN       NaN           NaN       NaN         NaN
shipping_limit_date                                                    object  112650        0          0.0   93318    0.828389   2017-07-21 18:25:23     21.0  0.000186   NaN      NaN         NaN     NaN         NaN    NaN       NaN       NaN           NaN       NaN         NaN
price                                                                 float64  112650        0          0.0    5968    0.052978                   NaN      NaN       NaN  0.85  6735.00  120.653739   74.99  183.633928    0.0    0.0000       0.0           0.0  7.923208  120.828298
freight_value                                                         float64  112650        0          0.0    6999    0.062130                   NaN      NaN       NaN  0.00   409.68   19.990320   16.26   15.806405  383.0    0.0034       0.0           0.0  5.639870   59.788253

存在一些缺失率低于 3% 的字段,不影响整体分析,无需处理。

在 TableauBI 中对 GMV 可视化处理后,可以明显发现 GMV 持续高波走低,经营情况不容乐观。

olist_bi_gmv

发现问题后,需要找问题根因,手段是进一步拆分 GMV,看到底是哪个子指标导致 GMV 持续走低。

这里的拆分方式有很多,但是根据 MECE 原则,我们一般优先把 GMV 拆分为 $订单量 \times 订单均价$。

对比订单量和订单均价与 GMV 的定基变化率,可以明显看到 GMV 几乎完全是由订单量驱动的。

olist_bi_gmv_split

也就是说,GMV 的异常趋势主要是订单量出了问题。那为什么订单量会出问题。

考虑到 Olist 当时作为初创公司的背景,可以往两个方面考虑:新用户少了或者老用户走了。

所以我们可以进一步拆分:$订单量 = 新用户订单 + 老用户订单$

列出新老用户订单量的逐月占比,可以清晰地观察到,几乎所有订单都来自新用户,老用户复购比例极低。

olist_bi_user_type

那现在的问题就更清晰了,新用户开发接近瓶颈,但老用户又留不下来。那用户为什么不复购呢?

用户不复购的理由无非就两种:商品本身不需要频繁复购或者购买体验差导致不愿意再复购。

对比新老用户的商品购买偏好,可以发现没有明显区别,但买的东西大多数都是耐用品,本身复购率就比较低。

olist_bi_product_cate

英文分类名 中文含义 产品属性 复购频率
bed_bath_table 床上用品、卫浴、桌布 半耐用品 。一套床单或毛巾通常可以使用 1-2 年。
sports_leisure 运动与休闲(器材、服装) 耐用品/消费品 中低。运动器材(如哑铃、帐篷)属于典型耐用品。
furniture_decor 家具装饰(灯具、挂饰等) 硬耐用品 极低。装修或布置房间是一次性需求。
health_beauty 健康美容(仪器、化妆品) 消费品 。虽然护肤品是消耗品,但 Olist 上有很多是美容仪器。
housewares 家用器具(厨具、小家电) 耐用品 。锅铲、烤箱等家电除非损坏,否则很少复购。

另一方面,我们还要看看那些非满分评价主要是在抱怨什么。

olist_bi_review_score

这里需要 NLP 接入进行情感分析,和主题词提取。

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
import os
import sys

# 配置 HuggingFace 镜像
os.environ['HF_ENDPOINT'] = 'https://hf-mirror.com'

import nltk
import pandas as pd

# 配置打印时不折叠字段
pd.set_option('display.width', None)
pd.set_option('display.max_columns', None)

from bertopic import BERTopic
from nltk.corpus import stopwords
from sklearn.feature_extraction.text import CountVectorizer


def ensure_nltk_resources():
    """
    确保 NLTK 资源存在,用于停用词处理

    :return: None
    """
    try:
        nltk.find('corpora/stopwords')
    except LookupError:
        print('正在尝试下载 NLTK stopwords...')
        try:
            nltk.download('stopwords')
        except Exception as e:
            print(f'\nNLTK 资源下载失败:{e}')
            sys.exit(1)


def sense_analysis():
    """
    情感分析处理

    :return: None
    """
    print("--- 程序已启动,正在初始化,请稍候... ---")
    # 1. 确保 NLTK 停用词可用
    ensure_nltk_resources()
    pt_stopwords = stopwords.words('portuguese')
    # 剔除无意义的口水词
    extra_stopwords = ['ol', 'bo', 'bababa', 'jjjj', 'io', 'td', 'ta', 'bommm']
    pt_stopwords.extend(extra_stopwords)
    # 2. 读取并清洗数据
    reviews = pd.read_csv('./dataset/olist_order_reviews_dataset.csv')
    reviews = reviews.dropna(subset=['review_comment_message'])
    reviews = reviews[reviews['review_comment_message'].str.strip() != '']
    reviews = reviews.loc[reviews['review_score'] < 5, :]
    reviews['clean_text'] = reviews['review_comment_message'].str.lower()
    print(f'评论清洗完成,有效评论量: {len(reviews)}')
    # 3. 初始化 BERTopic
    print('正在初始化 BERTopic 模型...')
    vectorizer_model = CountVectorizer(stop_words=pt_stopwords)
    topic_model = BERTopic(
        language='multilingual',
        embedding_model='sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2',
        vectorizer_model=vectorizer_model,
        verbose=True,
        nr_topics='auto'
    )
    # 训练模型
    topics, probs = topic_model.fit_transform(reviews['clean_text'])
    # 4. 获取主题列表
    topic_info = topic_model.get_topic_info()
    print('\n主要主题列表 (Top 20):')
    print(topic_info.head(20))
    top_20_topics = topic_info.head(20).copy()
    # 6. 导出结果
    # 将主题 ID 映射回原始数据
    output_path = './dataset/bad_reviews_top_topics.csv'
    top_20_topics.to_csv(output_path, index=False)
    print(f'\n结果已导出至: {output_path}')


if __name__ == '__main__':
    sense_analysis()
主题 ID 样本数量 主题标签 (Name) 关键词 (Representation) 代表性评论内容 (中译)
-1 8875 未分类/综合投诉 产品, 购买, 物流, 店铺, 收到, 期限 “订单显示已送达但我没收到”, “提前送达且质量很好”, “只收到了两个中的一个”
0 534 数量缺失 单位, 两件, 仅, 收到, 购买, 零件 “买了2个单位只收到1个!!”, “我买了两个,结果只发了一个。”
1 526 总体好评 好, 棒, 推荐, 不错, 喜欢, 顶级的 “好”, “很好”, “非常推荐”
2 507 提前送达(质量) 提前, 快速, 期限, 收到, 质量 “比预期提前送达,产品非常好!”, “产品提前到了。”
3 410 配送时效 提前, 期限, 延迟, 准时, 延期 “提前到货”, “在期限内送达”, “物流稍有延迟”
4 371 包装破损 箱子, 错误, 压扁, 包装, 缺陷, 损坏 “包装很差,箱子都压扁了”, “产品收到时包装盒是坏的。”
5 346 颜色错误 颜色, 蓝色, 黑色, 错误, 粉色, 白色 “颜色不对”, “买了粉色结果发了黑色。虽然产品不错但颜色错了。”
6 341 优质/推荐 棒, 优秀, 质量, 漂亮, 推荐 “产品很好”, “质量非常好的产品”
7 289 逾期未达(日期) 2018, 日期, 预计, 今天, 还没收到 “预计27号送达,今天29号了还没收到!”, “一直没收到货也没发票。”
8 285 邮局自提问题 家里, 邮局, 去取, 运费, 住宅 “我付了送货上门的运费,却得自己去邮局取货!”, “产品没送到家。”
9 258 手表类目 手表, 卡西欧, 表带, 女款, 盒子 “手表非常好”, “没收到手表”, “手表没送达”
10 234 订单查询/无回应 订单, 包裹, 还没, 联系, 没人 “还没收到我的订单”, “联系不上客服,订单还没到”
11 212 订单取消 取消, 申请, 没收到, 已经 “逾期未送达我申请取消”, “没收到货也没要求取消”, “申请了取消结果还是发货了”
12 182 状态正常 OK, 一切正常, 正常, 顺利 “一切 OK”, “全部正常”, “符合预期”
13 167 性价比/有用 好, 比较, 价格, 柔软, 实用 “非常好”, “非常实用”, “考虑到价格很不错”
14 163 无法评价(未收到) 还没, 收到, 目前, 无法评价 “目前还没收到产品,无法给出评价”, “还没收到货”
15 162 店铺推荐 店铺, 推荐, 棒, 购买, 恭喜 “推荐这家店”, “店铺和产品都值得推荐”
16 153 家具/椅子类 椅子, 靠背, 组装, 螺丝, 缺少 “少发了一把椅子”, “椅子不错”, “买了5把目前只收到了4把”
17 146 物流极速 快速, 效率, 速度, 超快, 服务 “物流非常快”, “送货效率很高”
18 138 退款/未收到 收到, 没收到, 退款, 完整 “没收到产品”, “要求全额退款”

使用 BERTopic 进行自然语言情感分析后,发现买家的抱怨主要集中在物流方面,我们需要进一步衡量物流指标。

观察到,实际的数据没有预想的糟糕,多数时候还是可以准时送达的。

图中 17 年 11 月是黑色星期五,物流压力陡增导致延迟率上升;另外,18 年 2 月和 3 月是巴西物流工人大罢工,全国邮政基本停摆,属于不可抗力导致的物流延迟率上升。

olist_bi_is_delayed

This post is licensed under CC BY 4.0 by the author.