一、背景目标
Shopee(虾皮网)是东南亚电商平台,覆盖新加坡、马来西亚、菲律宾、泰国、越南、巴西、墨西哥、哥伦比亚、智利等十余个市场,触达超10亿消费者!2023年Shopee总订单量达82亿,23年Q4总订单数同比增长46%!
分析数据样本来自某爬虫系统爬取的Shopee网从2023年4月至2023年5月期间特定产品的销售数据。
任务要求
任务要求:从数据中获取在2023年5月上市的产品。
使用问题1中的清理数据来执行下一个任务。
1、显示每天爬取的产品数量。
2、显示基于位置的上市产品数量。你可以从“规格”字段中提取这个信息。
- 如果它来自马来西亚的任何州,使用州名(如雪兰莪、柔佛、马六甲等)作为标签。
- 如果它来自马来西亚之外的地方,使用“海外”作为标签。
商品类别详情可能有如下格式:“Shopee | 女装 | 外套 | 大衣 & 夹克”。我们可以将其拆分为:
- 主要类别:女装
- 子类别 1:外套
- 子类别 2:大衣 & 夹克
a. 显示基于主要类别的上市产品数量。
b. 对于前3个主要类别,根据产品数量显示该主要类别下的前5个子类别 1
3、显示每个主要类别的价格范围。
4、按降序显示每个主要类别的收入。
二、数据探索分析
2.1 数据概况
-
数据时间范围: 2012年4月1日至2014年3月31日的数据
-
数据记录数:20312行
-
字段数:20个
-
数据属性说明(字段)
字段 | 含义 |
---|---|
price_ori
| 价格 |
delivery
| 交付送达 |
item_category_detail
| 项目类别详细信息 |
specification
| 规格 |
title
| 标题 |
w_date
| 日期 |
link_ori
| 链接 |
item_rating
| 评分 |
seller_name
| 卖家名称 |
idElastic
| 理想的 |
price_actual
| 当前价格 |
sitename
| 站点名称 |
idHash
| id的hash值 |
total_rating
| 总评分 |
id
| 订单id,与idHash相同 |
total_sold
| 售出总数 |
pict_link
| 图片链接 |
favorite
| 收藏数 |
timestamp
| 时间戳 |
desc
| 描述 |
三、数据预处理
3.1 重复值分析
# 查询是否有重复值
df.duplicated().sum()
3.2 缺失值分析
-
检查每一列的缺失值,并降序排列
存在大量缺失值,但我们需要对缺失值字段注意分析,其中
delivery是交付送达情况,该值为空时,说明商品还未送达用户手中,属于正常情况;
specification、seller_name文本类型,且本次分析无需使用,可以忽略;
favorite是收藏数,观察数据,该字段是字符串型,为空时未有记录,不过可以将na填充为0
df['favorite'].fillna('0', inplace = True)
item_rating和total_rating字段是评分字段,且为空值记录数较小,将na填充为0
df['item_rating'].fillna('0', inplace=True)
df['total_rating'].fillna('0', inplace=True)
price_ori 和 price_actual字段是与价格相关的字段,这里需要进行缺失值的处理,处理方式是
- 删除同时为空的记录
- 删除同时为0的记录
- 存在一个空字段的记录,这里用另外一个字段值填充
df = df.dropna(subset=['price_ori', 'price_actual'], how='all')
df = df[(df['price_ori'] != 0) & (df['price_actual'] != 0)].copy()
df['price_ori'].fillna(df['price_actual'], inplace=True)
df['price_actual'].fillna(df['price_ori'], inplace=True)
处理完成后的情况如下图所示:
3.3 新增字段
通过观察数据中specification字段的值,发现该字段均有 Ships From 子串,我们可以将此作为字符串分隔符,拆分出商品发货地,例如:
Product Specifications Category Shopee Men Clothes Traditional Wear Bottoms Stock 52 Ships From Mainland China
发货地为:Mainland China(中国大陆)
# 提取发货地
def extract_ship_from_spec(spec):
if isinstance(spec, str):
parts = spec.split('Ships From ')
if len(parts) > 1:
return parts[1:][0]
else:
return 'No state'
else:
return 'No info'
df['shipfrom'] = df['specification'].apply(extract_ship_from_spec)
四、数据分析
4.1 任务一
获取在2023年5月上市的产品,并获取每天爬取的产品数量。
df["w_date"] = pd.to_datetime(df["w_date"])
df_may2023 = df[( (df['w_date'].dt.year==2023) & (df['w_date'].dt.month==5) )]
df_crwal_bydate = df_may2023.groupby('w_date')['id'].count()
plt.figure(figsize=(10, 6))
plt.plot(df_crwal_bydate.index, df_crwal_bydate.values)
plt.xlabel('爬取日期')
plt.ylabel('爬取产品数')
plt.title('每天爬取 Shopee 的产品')
plt.show()
4.2 任务二
显示基于位置的上市产品数量,通过观察数据,部分specification 中包含有 发货地信息,我们将从specification字段中截取发货地信息来进行分析
def extract_ship_from_spec(spec):
if isinstance(spec, str):
parts = spec.split('Ships From ')
if len(parts) > 1:
x = parts[-1].split()
return x[-1]
else:
return 'No state'
else:
return 'No info'
# 将截取截取到信息保存在新增列shipfrom中
df_may2023.loc[:,'shipfrom'] = df_may2023['specification'].apply(extract_ship_from_spec)
# 根据任务要求,马来西亚的州名作为标签,其他全部以Oversea表示,这里我做了调整,将中国内地和中国台湾作为标签,其他以Oversea表示。
replacements = {
'Taiwan': 'Taiwan China',
'China': 'Mainland China',
'Indonesia': 'Oversea',
'Thailand': 'Oversea',
'Vietnam': 'Oversea',
'Korea': 'Oversea',
'ID': 'Oversea',
'No state': 'Oversea',
'No info': 'Oversea'
}
df_may2023.loc[:,'shipfrom'] = df_may2023['shipfrom'].replace(replacements)
df_shipfrom_count = df_may2023['shipfrom'].value_counts().sort_values(ascending=True)
plt.figure(figsize=(14, 8))
df_shipfrom_count.plot(kind = 'barh', color='skyblue', edgecolor='black', title = '2023年5月产品数量统计(by发货地)')
plt.xlabel('产品数量')
plt.ylabel('发货地')
for i in range(len(df_shipfrom_count)):
plt.text(df_shipfrom_count.iloc[i], i, f'{df_shipfrom_count.iloc[i]}', ha='left', va='center')
plt.show()
4.3 任务三
拆分出商品主要类别、子类别 1、子类别 2
def extract_main_category(category_detail, level):
if pd.isna(category_detail):
return -1
categories = category_detail.split('|')
categories = [category.strip() for category in categories[1:]]
if level <= 0 or level > len(categories):
raise ValueError("请求的级别不合法或超出了分类的总数")
return -2
if len(categories) > 0:
return categories[level - 1]
df_may2023.loc[:, 'Main_Category'] = df_may2023['item_category_detail'].apply(extract_main_category, level=1)
df_mainCatagory_count = df_may2023['Main_Category'].value_counts()
df_mainCatagory_count
a) 主要分类及产品数量
b)前3个主要类别,根据产品数量显示该主要类别下的前5个子类别 1
main_top3 = df_mainCatagory_count.nlargest(3)
for catagory in main_top3.index:
catagory_data = df_may2023[ df_may2023['Main_Category'] == catagory]
catagory_1_count = catagory_data['item_category_detail'].apply(extract_main_category, level=2).value_counts().nlargest(5)
print(catagory_1_count)
结果如下
4.4 任务四
显示每个主要类别的价格范围
这里特别说一下,通过min、max、均值和中位数获得的结果显示,价格数据质量很差,无论采用哪个值都将存在很大的偏差。
df_price_range = df_may2023.groupby('Main_Category')['price_actual'].agg(['min','mean','median','max']).reset_index()
df_price_range.columns=['Main_Category','最低价','均价','中位数','最高价']
for column in ['最低价', '均价','中位数', '最高价']:
df_price_range[column] = df_price_range[column].map('{:,.2f}'.format)
df_price_range
整体上来看,均价数值相差量级,中位数的数据相对合理,所以这里采用中位数来绘制条形图。
4.5 任务五
按降序显示每个主要类别的收入。
df_revenue_category = df_may2023.groupby('Main_Category')['price_actual'].sum() \
.sort_values(ascending=False) \
.reset_index() \
.rename(columns={'price_actual': '收入'})
df_revenue_category
五、小结
本次项目任务明确,在分析过程中,数据处理环节较为重要,注意事项总结如下:
1、对于价格字段的处理,实际价格和标价都存在空值,所以需要进行相应的处理
- 2个字段全部为空或全部为0的记录,删除
- 2个字段中有1个为空时,用另一个字段值填充
2、遇到了排序无效的问题,初步排查原因是价格字段中有表示货币的数值格式(即带有千位分隔符,),进行排序时乱序排列,解决办法是将类型转换为字符串后,删除逗号再转换回float类型。
3、尽量使用 .loc
来进行行和列的索引,以避免SettingWithCopyWarning
警告
4、发货地部分,采用了最粗暴的方法,若仔细观察数据,实际上是可以继续清洗,以获得更好的数据结果。当然,由于数据是文本型,商家在填写specification时也并非规范,没有必要问题复杂化。