1 大数据集群
1.1 大数据集群介绍
Spark集群是一种分布式计算框架,它基于内存计算,提供了高效的数据抽象和并行计算能力,能够处理大规模数据集的批处理和实时处理任务。Spark采用内存存储中间计算结果,可减少迭代运算的磁盘I/O,并通过并行计算有向无环图的优化,使其运行速度比MapReduce快100倍;Spark可以使用Hadoop YARN和Apache Mesos作为其资源管理和调度器,可以从多种数据源读取数据,如HDFS、HBase、MySQL等。
PySpark则是Spark的Python API,允许Python开发者利用Spark的强大功能进行数据处理和分析。本案例采用PySpark大数据集群做数据分析与挖掘,使用HDFS作为文件存储系统。PySpark由多个组件构成,包括Spark SQL、Spark Streaming、MLlib(机器学习库)和GraphX(图计算库)等。
本案例部署了3个节点的完全分布式集群,具体的开发环境如下:
节点/组件/安装包 | 版本 | 备注 |
---|---|---|
名称节点 | 192.168.126.10 | master |
数据节点 | 192.168.126.11 | slave1 |
数据节点 | 192.168.126.12 | slave2 |
JDK | jdk-8u281 | Java运行环境,Spark的运行需要JDK的支持 |
Hadoop | hadoop-3.1.4 | 提供HDFS、Hive运行环境支持。HDFS系统访问端口为:hdfs://192.168.126.10:9000 |
pyspark | spark-3.4.3-bin-hadoop3.tgz | Spark集群的master节点的地址和端口为:spark://192.168.126.10:7077 |
MySQL | 5.7.18 | 存储数据分析结果,端口:3306,用户名:root,密码:123456 |
python | Python-3.9.0.tgz | 3.9.0版本的Python |
MySQL Connector | mysql-connector-java-5.1.32-bin.jar | Spark 连接MySQL的驱动 |
IntelliJ IDEA | Ultimate 2020.3 | 编程工具IDEA |
大数据集群运行时,Spark的管理页面如下图所示:
1.2 大数据集群部署、相关组件(软件)的配置(略,后续更新)
2 购物篮数据分析与挖掘
2.1 数据集介绍
“Assignment-1_Data”数据集,包含与消费者购物行为相关的数据,用于市场购物篮分析(MBA)和关联规则挖掘(Association Rule Mining),目的是通过分析消费者在购买过程中的商品组合,揭示商品之间的关联性和购买行为模式。
数据集下载地址: 购物篮数据集
2.1.1 数据集包含的内容
数据集的字段如下图所示:
字段 | 说明 | 备注 |
---|---|---|
订单号(BillNo) | 用于唯一标识每一笔交易或订单。 | 订单信息 |
购买日期(Date) | 记录订单发生的日期 | 订单信息 |
客户ID(CustomerID) | 用于标识购买该订单的客户,可用于进一步分析客户的购买习惯和偏好。 | 订单信息 |
商品名称(Itemname) | 列出订单中包含的商品名称。 | 商品信息 |
数量(Quantity) | 每种商品在订单中的购买数量。 | 商品信息 |
单价(Price) | 每种商品的单价 | 商品信息 |
国家(Country) | 交易地点 | 订单信息 |
数据集有7个字段,522,064条记录,共38.7MB。
2.1.2 购物篮分析(MBA)与关联规则挖掘(Association Rule Mining)的应用
(1)识别频繁项集:
支持度(Support):衡量某个商品组合在所有订单中出现的频率。高支持度意味着该商品组合在销售中较为常见。例如,如果“可乐+薯片”组合的支持度为10%,则意味着在所有订单中,有10%的订单同时包含了这两种商品。
(2)提取关联规则:
置信度(Confidence):衡量在购买了商品A的条件下,同时购买商品B的概率。高置信度表示商品之间的关联性强。
提升度(Lift):衡量商品A的购买对商品B购买的影响程度。提升度大于1表示商品A的购买对商品B的购买有正向影响。例如,如果购买可乐的顾客中有40%也购买了薯片,而薯片在所有订单中的支持度为10%,则“可乐→薯片”的置信度为40%,提升度为4(因为40%/10%=4),表示购买可乐对购买薯片有很强的正向影响。
(3)应用分析结果:
交叉销售和捆绑销售:根据分析出的关联规则,商家可以设计交叉销售策略,将经常一起购买的商品放在相邻的货架上,或进行捆绑销售以提高销售额。
库存管理和促销策略:了解哪些商品组合更受欢迎,有助于商家优化库存管理和制定更有效的促销策略。
个性化推荐:在电子商务平台上,可以根据用户的购买历史和行为,利用关联规则为其推荐可能感兴趣的商品。
2.1.3 数据集上传
先启动Hadoop集群和Spark集群,再将数据集Assignment-1_Data.csv上传到Hadoop分布式存储系统HDFS的目录/data/下,如下图所示:
2.2 数据导入,数据预处理
打开IntelliJ IDEA,新建项目flaskpy,再新建python文件:购物篮数据分析挖掘_关联规则.py,以及其他文件夹(Flask部分再详细说明),项目结构如图所示。
打开python文件:购物篮数据分析挖掘_关联规则.py。
数据导入:先导入数据集,再做数据预处理。
数据预处理:主要是日期(Date)列拆分转换为年(Year)、月(Month)、日(Day)、星期(DayOfWeek)、小时(Hour)、分钟(Minute)列,为后面更深、更细的数据分析和挖掘做准备;删除负值的记录、填充缺失值并删除那些非商品的记录。
from pyspark import SparkConf
from pyspark.ml.feature import StringIndexer
from pyspark.sql import SparkSession
from pyspark.sql.functions import *
from pyspark.ml.recommendation import ALS
from pyspark.ml.evaluation import RegressionEvaluator
from pyspark.ml.fpm import *
import matplotlib.pyplot as plt
import pandas as pd
from pyspark.sql.types import StructField, IntegerType
plt.rcParams['font.sans-serif'] = ['SimHei'] # 指定默认字体为SimHei显示中文
plt.rcParams['axes.unicode_minus'] = False # 解决保存图像时负号'-'显示为方块的问题
# 创建一个Spark配置对象,用于设置Spark作业的配置项
conf = SparkConf().setAppName("购物篮分析") \
.set("spark.sql.legacy.timeParserPolicy", "LEGACY") \
.setMaster('spark://192.168.126.10:7077') # Spark集群的master节点的地址和端口
sc = SparkContext.getOrCreate(conf)
spark = SparkSession(sc)
# -------------一、数据导入与数据预处理-----------------------
# ------------(一)数据导入--------------------
filename = "Assignment-1_Data.csv"
data = spark.read.csv('hdfs://192.168.126.10:9000/data/' + filename, header=True, inferSchema=True) #inferSchema=True表示自动推断每列的数据类型
data.show(10, False) # 显示数据的前10行,并设置False来避免截断过长的列值
data.printSchema() # 打印数据的模式(Schema),即各列的名称和数据类型
# ------------(二) 数据预处理--------------
# ----------1 数据转换------------
data = data.withColumn('Timestamp', to_timestamp('Date', 'yyyy/MM/dd HH:mm')) # 将'Date'列的数据类型从字符串转换为时间戳类型,并添加列Timestamp
data.show(5)
print("数据集记录数:", data.count())
# 接下来,基于新创建的'Timestamp'列,提取并添加年、月、日、星期几、小时和分钟等字段
data = data.withColumn('Year', year('Timestamp')) \
.withColumn('Month', month('Timestamp')) \
.withColumn("Day", dayofmonth("Timestamp")) \
.withColumn("DayOfWeek", dayofweek("Timestamp")) \
.withColumn("Hour", hour("Timestamp")) \
.withColumn("Minute", minute("Timestamp"))
data.show(5)
# -----2 删除负值的记录、填充缺失值并删除那些非商品的记录-------------
data = data.where((data['Quantity'] > 0) & (data['Price'] > 0)) # 删除商品数量和价格为负数的记录
print(data.count())
data = data.withColumn("Total_Price", col("Quantity") * col("Price")) # 增加一列商品总价Total_Price
data_null_agg = data.agg(*[sum(when(isnull(c), 1)).alias(c) for c in data.columns]) # 检查并查看每列的空值总数
data_null_agg.show() # 发现 CustomerID 有十几万个空值
data = data.fillna('99999', 'CustomerID') # 客户号CustomerID 空值填充99999
data = data.where(
~(data['Itemname'].isin(['POSTAGE', 'DOTCOM POSTAGE', 'Adjust bad debt', 'Manual']))
) # 删除非商品的那些Item
data.show(5)
2.3 数据探索
2.3.1 总体销售情况
从订单量(No_Of_Trans)、成交量(Quantity)与成交额(Total_Price)三个维度,分别从多个角度进行数据分析与可视化。数据可视化不是Spark的强项,这里仅用于程序测试,程序最终提交集群运行时,可以将数据可视化部分放在Flask部分,使用PyEchart将多张图片做成Web端可视化大屏,方便用户查看。
编程思路:
(1)总体交易分析(data_ttl_trans):按订单号(BillNo)分组,计算各组的成交量(Quantity)和成交额(Total_Price)。然后将计算结果存储到MySQL,再用PyEcharts绘图。同时为了测试需要,将计算结果(PySpark DataFrame格式)转换为 Pandas DataFrame格式,用于测试程序的可视化绘图。
(2)总体商品分析(data_ttl_item):按商品名称(Itemname)分组,计算各组的成交量(Quantity)和成交额(Total_Price)。
(3)总体时间分析(按年和月)(data_ttl_time):按年(Year)、月(Month)分组,计算各组的成交量(Quantity)和成交额(Total_Price)。
(4)总体星期几分析(data_ttl_weekday):按星期(DayOfWeek)分组,计算各组的成交量(Quantity)和成交额(Total_Price)。
(5)总体国家分析(data_ttl_country):按国家(Country)分组,计算各组的成交量(Quantity)和成交额(Total_Price)。
(6)总体国家时间分析(按国家、年和月)(data_ttl_country_time):按国家(Country)、年(Year)、月(Month)分组,计算各组的成交量(Quantity)和成交额(Total_Price)。
最后将6个分组计算后的结果存储到MySQL,同时将其转换为Pandas的DataFrame,但是Pandas的DataFrame的数据运算只能在driver节点进行,会降低整个Spark集群性能,因此只适合处理小型数据集。而PySpark的Data Frame的数据是分布式集群计算,适合处理中大规模数据集。程序代码如下:
# ----------------二、探索性分析--------------------
# ------(一)总体销售情况-----
# 从订单量(No_Of_Trans)、成交量(Quantity)与成交额(Total_Price)三个维度,分别从多个角度进行数据可视化与分析
# 1.总体交易分析
data_ttl_trans = data.groupby("BillNo").agg(
sum("Quantity").alias("Quantity"),
sum("Total_Price").alias("Total_Price"))
data_ttl_trans_copy = data_ttl_trans # 用于存储到MySQL
data_ttl_trans = data_ttl_trans.toPandas() # 如果需要Pandas DataFrame,则转换为Pandas
print("\n总体交易分析:\n", data_ttl_trans)
# 2.总体商品分析
data_ttl_item = data.groupby("Itemname").agg(
sum("Quantity").alias("Quantity"),
sum("Total_Price").alias("Total_Price"))
data_ttl_item_copy = data_ttl_item
data_ttl_item = data_ttl_item.toPandas() # 如果需要Pandas DataFrame,则转换为Pandas
print("\n总体商品分析:\n", data_ttl_item)
# 3.总体时间分析(按年和月)
data_ttl_time = data.groupby(["Year", "Month"]).agg(
sum("Quantity").alias("Quantity"),
sum("Total_Price").alias("Total_Price"))
data_ttl_time_copy = data_ttl_time
data_ttl_time = data_ttl_time.toPandas() # 如果需要Pandas DataFrame,则转换为Pandas
print("\n总体时间分析(按年和月):\n", data_ttl_time)
# 4.总体星期几分析
data_ttl_weekday = data.groupby("DayOfWeek").agg(
sum("Quantity").alias("Quantity"),
sum("Total_Price").alias("Total_Price"))
data_ttl_weekday_copy = data_ttl_weekday
data_ttl_weekday = data_ttl_weekday.toPandas() # 如果需要Pandas DataFrame,则转换为Pandas
print("\n总体星期几分析:\n", data_ttl_weekday)
# 5.总体国家分析
data_ttl_country = data.groupby("Country").agg(
sum("Quantity").alias("Quantity"),
sum("Total_Price").alias("Total_Price"))
data_ttl_country_copy = data_ttl_country
data_ttl_country = data_ttl_country.toPandas() # 如果需要Pandas DataFrame,则转换为Pandas
print("\n总体国家分析:\n", data_ttl_country)
# 6.总体国家时间分析(按国家、年和月)
data_ttl_country_time = data.groupby(["Country", "Year", "Month"]).agg(
sum("Quantity").alias("Quantity"),
sum("Total_Price").alias("Total_Price"))
data_ttl_country_time_copy = data_ttl_country_time
data_ttl_country_time = data_ttl_country_time.toPandas() # 如果需要Pandas DataFrame,则转换为Pandas
print("\n总体国家时间分析(按国家、年和月):\n", data_ttl_country_time)
# ----------------将数据存储到Linux的MySQL中,为可视化大屏做数据准备-----------------------
# 定义MySQL数据库的连接参数:数据库为test,用户名root,密码123456,驱动com.mysql.jdbc.Driver
url = "jdbc:mysql://192.168.126.10:3306/test"
properties = {
"user": "root",
"password": "123456",
"driver": "com.mysql.jdbc.Driver"}
# 写数据到MySQL的test数据库的movie表,无需预先在MySQL创建表。自动在mysql创建movie表
data_ttl_trans_copy.coalesce(1).write.format('jdbc') \
.option('url', url) \
.option('dbtable', 'data_ttl_trans') \
.option('user', properties['user']) \
.option('password', properties['password']) \
.option('driver', properties['driver']) \
.option('mode', 'overwrite') \
# .save() # 第一次建表时使用.save()。append为追加数据模式,overwrite为覆盖数据模式
print('Spark将数据写入mysql完成。')
# ---------封装将pyspark的dataframe写入mysql数据库的函数,方便调用--------------------
from pyspark.sql import DataFrame
import pymysql
def write_to_mysql(df: DataFrame, url: str, properties: dict, table_name: str):
"""
将Pyspark的DataFrame写入MySQL数据库的指定表。检查表是否存在,存在则先删除表。
参数:
- df: 要写入的DataFrame。
- url: MySQL数据库的JDBC URL。
- properties: 包含数据库连接信息的字典(user, password, driver)。
- table_name: 要写入的MySQL表名。
"""
conn = pymysql.connect(host=url.split('/')[2].split(':')[0],
port=int(url.split('/')[2].split(':')[1].split('/')[0]),
user=properties['user'],
password=properties['password'],
database=url.split('/')[-1],
charset='utf8mb4',
cursorclass=pymysql.cursors.DictCursor)
try:
with conn.cursor() as cursor:
# 检查表是否存在
cursor.execute(f"SHOW TABLES LIKE '{table_name}'")
if cursor.fetchone():
# 如果表存在,则删除它
cursor.execute(f"DROP TABLE IF EXISTS {table_name}")
conn.commit()
print(f"删除表:{table_name}")
finally:
conn.close()
# 写入数据到MySQL
df.coalesce(1).write.format('jdbc') \
.option('url', url) \
.option('dbtable', table_name) \
.option('user', properties['user']) \
.option('password', properties['password']) \
.option('driver', properties['driver']) \
.option('mode', 'append') \
.save()
print(f'Spark将数据写入MySQL的{table_name}表完成。')
# --------封装函数完成。-------
# 使用示例
url = "jdbc:mysql://192.168.126.10:3306/test" # test为MySQL的数据库
properties = {
"user": "root", # MySQL用户名
"password": "123456", # MySQL密码
"driver": "com.mysql.jdbc.Driver" }# MySQL驱动
# 将相关的数据分析结果存储到Linux的MySQL,用于windows端数据可视化。
write_to_mysql(data_ttl_item_copy, url, properties, 'data_ttl_item') # 2.总体商品分析
write_to_mysql(data_ttl_time_copy, url, properties, 'data_ttl_time') # 3.总体时间分析(按年和月)
write_to_mysql(data_ttl_weekday_copy, url, properties, 'data_ttl_weekday') # 4.总体星期几分析
write_to_mysql(data_ttl_country_copy, url, properties, 'data_ttl_country') # 5.总体国家分析
write_to_mysql(data_ttl_country_time_copy, url, properties, 'data_ttl_country_time') # 6.总体国家时间分析(按国家、年和月)
# 后面其他PySpark DataFrame数据需要写入MySQL的,直接调用函数即可。Pandas DataFrame数据不能调用该函数。
在IDEA的调试模式下,可以查看Pandas的DataFrame数据。
2.3.2 异常值检测与处理
异常值检测与处理:用箱线图对分组后的数据做异常值检测,删除异常值。
使用np.percentile函数计算特定百分位数,箱线图的异常值上界和下界通常定义为Q3 + 1.5IQR(上界)和Q1 - 1.5IQR(下界),其中Q1是第一四分位数(25%分位数),Q3是第三四分位数(75%分位数),IQR = Q3 - Q1是四分位距。
代码如下:
# -------------------------------------(二)异常值检测及处理-------------------------------------------------------------------
# data_ttl_trans["Quantity"].plot(kind='boxplot')
plt.boxplot(data_ttl_trans["Quantity"]) # 绘制总体交易成交量的箱线图,查看异常值
plt.show()
plt.boxplot(data_ttl_trans["Total_Price"]) # 绘制总体交易成交额的箱线图,查看异常值
plt.show()
# 销售数据极不平衡,有些单子里的销售额或销售量远远超过正常标准,因此在作分布图时,将箱线图中的离群点去除。
"""
np.percentile函数用于计算特定百分位数,而箱线图的异常值上界和下界通常定义为Q3 + 1.5*IQR(上界)和Q1 - 1.5*IQR(下界),
其中Q1是第一四分位数(25%分位数),Q3是第三四分位数(75%分位数),IQR = Q3 - Q1是四分位距。
"""
# 计算四分位数
Q1_quantity = np.percentile(data_ttl_trans["Quantity"], 25) # 25%分位数
Q3_quantity = np.percentile(data_ttl_trans["Quantity"], 75) # 75%分位数
IQR_quantity = Q3_quantity - Q1_quantity # 四分位距
# 计算箱线图异常值上界(注意:这里只计算了上界,下界可以类似计算)
upper_bound_quantity = Q3_quantity + 1.5 * IQR_quantity
print("成交量箱线图上界:", upper_bound_quantity)
# 对Total_Price做同样的计算
Q1_total_price = np.percentile(data_ttl_trans["Total_Price"], 25)
Q3_total_price = np.percentile(data_ttl_trans["Total_Price"], 75)
IQR_total_price = Q3_total_price - Q1_total_price
upper_bound_total_price = Q3_total_price + 1.5 * IQR_total_price
print("成交额箱线图上界:", upper_bound_total_price)
# 使用histplot绘制分布图
# 注意:这里假设了某个阈值来限制绘图的数据范围,但这通常不是必需的,除非您想要专注于数据的某个子集
plt.hist(data=data_ttl_trans[data_ttl_trans["Quantity"] <= upper_bound_quantity], x="Quantity")
plt.title("Distribution of Quantity (limited)")
plt.show()
plt.hist(data=data_ttl_trans[data_ttl_trans["Total_Price"] <= upper_bound_total_price], x="Total_Price")
plt.title("Distribution of Total Price (limited)")
plt.show()
# 从分布图上分布看依旧明显右偏趋势,表明了有极少数的订单拥有极大的销售量与销售额,远超正常订单的水平。
# 总体商品成交量异常值检测 及分布图
plt.boxplot(data_ttl_item['Quantity']) # 总体商品成交量的箱线图
plt.show()
Q1_quantity = np.percentile(data_ttl_item["Quantity"], 25) # 25%分位数
Q3_quantity = np.percentile(data_ttl_item["Quantity"], 75) # 75%分位数
IQR_quantity = Q3_quantity - Q1_quantity # 四分位距
upper_bound_quantity = Q3_quantity + 1.5 * IQR_quantity # 计算箱线图异常值上界
print("成交量箱线图上界:", upper_bound_quantity)
plt.hist(data=data_ttl_item[data_ttl_item["Quantity"] <= upper_bound_quantity], x="Quantity")
plt.title("总体商品成交量的分布图")
plt.show()
成交量(Quantity)箱线图如下:
成交额(Total_Price)箱线图如下:
上两图所示,销售数据极不平衡,有些单子里的交易量或交易额远远超过正常标准,因此在作分布图时,将箱线图中的离群点去除。
剔除异常值后的成交量(Quantity)分布图如下:
剔除异常值后的成交额(Total_Price)分布图如下:
从分布图上看,分布依旧明显右偏趋势,表明了有极少数的订单拥有极大的销售量与销售额,远超正常订单的水平。
此处并没有删除数据的异常值。实际上,异常值检测与处理,应在数据分组前进行,也就是在“2.2 数据导入,数据预处理”部分,对原始数据做异常值检测与处理。后续将对这一点进行改进。
2.3.3 总体销售情况的数据可视化
数据可视化部分可以放在Windows端的Flask部分,用PyEcharts绘图,最后渲染到web页面,做成一个可视化大屏。
总体时间分析(按年和月)(data_ttl_time),可视化绘图如下:
可以看出,2011年12月,成交量和成交额都达到了最大值。
总体星期几分析(data_ttl_weekday),可视化绘图如下:
上图可以看出,周一的成交额较少,周二至周六的成交额起伏较为平缓,周日的成交额的数据缺失。
总体国家分析(data_ttl_country),绘制成交额排名前5的国家和剩余国家成交额的饼图如下:
总体上来看,总销售额(成交额)的绝大部分都是来自于英国销售,占比高达88.2%,紧随其后的是新西兰、德国、法国和澳大利亚,占总销售额的比例很小。
总体国家分析(data_ttl_country)中,比较每年每月英国成交额和其他国家成交额总和,绘制的堆叠柱形图如下:
由上图可以看出,从时间线上来看,每个月都是英国销售额占主体。
具体的代码如下:
# ----------------------------------(三)数据可视化(这部分可以放在windows端,用PyEcharts绘图)-------------------------
# ------------------1.data_ttl_time总体时间分析(按年和月)可视化-----------------------
data_ttl_time = data_ttl_time.sort_values(['Year', 'Month']) # 按年和月排序
data_ttl_time.index = [str(i) + "\n" + str(j) for i, j in zip(data_ttl_time["Year"], data_ttl_time["Month"])] # 重建索引
fig, ax1 = plt.subplots()
ax1.plot(data_ttl_time["Quantity"], color='gray', label="成交量")
ax1.set_ylabel("Quantity", color="gray")
plt.legend(loc='upper left')
ax2 = ax1.twinx() # 创建第二个y轴
ax2.plot(data_ttl_time["Total_Price"], color="red", label="成交额")
ax2.set_ylabel("Total_Price", color="red")
plt.legend(loc='upper right')
plt.show() # 从时间轴上来看,销售额有一个明显的上升趋势,到了11月有一个明显的高峰,而到12月则有所回落。
# --------------------------2.总体星期几分析 可视化-----------------------------
plt.bar(data_ttl_weekday["DayOfWeek"],
data_ttl_weekday["Total_Price"],
width=0.5, label="成交额")
plt.legend()
plt.show()
# -----------------------------3.总体国家分析可视化----------------------------
data_ttl_country.set_index("Country", inplace=True) # 设置索引为 Country 列
top_five = data_ttl_country["Total_Price"].sort_values(ascending=False)[:5] # 排序 Total_Price 并取前五个
others = data_ttl_country["Total_Price"].sort_values(ascending=False)[5:].sum() # 计算其他国家的总和
pie_data = top_five.copy()
pie_data = pie_data.append(pd.Series([others], index=["others"])) # 创建一个新的 Series 来包含前五个国家和“其他”的总和
# 绘制饼图
plt.pie(pie_data, labels=pie_data.index, autopct='%1.1f%%')
plt.axis('equal') # 确保饼图是圆形
plt.legend(loc="upper right")
plt.show()
# -----------------4.data_ttl_country_time总体国家时间分析(按国家、年和月)可视化-------------
data_ttl_country_time["isUnitedKingdom"] = ["UK" if i == "United Kingdom" else "Not UK" for i in
data_ttl_country_time["Country"]] # 创建isUnitedKingdom列
data_ttl_country_time_isUK = data_ttl_country_time.groupby(["isUnitedKingdom", "Year", "Month"])[
"Total_Price"].sum().reset_index() # 分组并计算成交额
data_ttl_country_time_isUK.index = [str(i) + "\n" + str(j) for i, j in
zip(data_ttl_country_time_isUK["Year"],
data_ttl_country_time_isUK["Month"])] # 重建索引
UK = data_ttl_country_time_isUK[data_ttl_country_time_isUK["isUnitedKingdom"] == "UK"] # 英国UK的每年每月成交额
NUK = data_ttl_country_time_isUK[data_ttl_country_time_isUK["isUnitedKingdom"] == "Not UK"] # 非英国UK的每年每月成交额
plt.figure(figsize=(8, 8))
plt.bar(UK.index, UK["Total_Price"], color="#66c2a5", label="英国")
plt.bar(NUK.index, NUK["Total_Price"], bottom=UK["Total_Price"], color="#8da0cb", label="其他国家")
plt.legend()
plt.show() # 总体上来看,总销售额的绝大部分都是来自于英国销售,而从时间线上来看,每个月都是英国销售额占主体。
2.3.4 英国是否存在季节性商品(按季度分组)
将2011年4个季度的交易量、成交量以及成交额分别汇总并排序,看看有没有哪个商品在各个季度的排名有明显的差距。也就是分别在交易量、成交量以及成交额找出季节性波动最大的一个商品(即:有的月份量大,有的月份量小),据此可以判断是不是季节性商品。
“No_Of_Trans”(不同购物小票的数量)、“Quantity”(成交量)和 “Total_Price”(成交额)。
尽量避免在Pandas和Spark之间频繁转换,因为Pandas DataFrame的操作通常不适合大规模数据集。尽量在Spark环境中完成所有的数据分析与处理。
编程思路如下:
1.选取英国2011年的数据,将1-12月转换为1-4季度。
2.确定每季度每件商品的交易订单量、成交量、成交额排名,给每件商品标注排名并添加新列。
3.选取四季度交易量、成交量、成交额3个维度排名的商品,四季度的前10商品名去重后按照维度放在一起。此时,存放的每个维度的商品数量(集合)有可能大于10件。
4.是否是季节性商品,也就是分析四季度排名前10的商品集合,分别在1、2、3、4季度的排名,然后计算四季度排名差,保存最大排名差。
如某商品PAPER CHAIN KIT VINTAGE CHRISTMAS (复古圣诞纸链套装)的交易量No_Of_Trans在第1、2、3、4季度的排名分别为1669、1734、110、4,值(交易量)分别为:11、9、143、509,因此该商品的排名差为:1734-4=1730。排名差越大,说明是季节性商品的可能性越大。
5.对交易订单量、销售量、销售额3个维度的商品排名差进行降序排序,选取排名差最大的3个商品。
6.分别绘制交易订单量、销售量、销售额3个维度排名差最大的3个商品四个季度的对应值的饼图。
代码如下:
# -------------------(四)英国是否存在季节性商品(按季度)-------------------------------
"""
将2011年的4个季度的成交量、销售量以及销售额分别汇总并排序,看看有没有哪个商品在各个季度的排名有明显的差距。
也就是分别在成交量、销售量以及销售额找出季节性波动最大的一个商品(有的月份量大,有的月份量小),据此可以判断它们是季节性商品。
"No_Of_Trans"(不同账单号的数量)、"Quantity"(每件商品的销售量)和 "Total_Price"(每件商品的销售额)。
尽量避免在Pandas和Spark之间频繁转换,因为Pandas DataFrame的操作通常不适合大规模数据集。如果可能,尽量在Spark环境中完成所有处理。
编程思路如下:
1.选取英国2011年的数据,将1-12月转换为1-4季度。
2.确定每季度每件商品的交易订单量、销售量、销售额排名,并添加列给每件商品标注排名。
3.选取四季度交易订单量、销售量、销售额3个维度排名的商品,四季度前10商品名去重后按照维度一起存放。此时,存放的每个维度的商品数量(集合)有可能大于10件。
4.是否存在季节性商品,也就是分析四季度排名前10的商品集合,分别在1、2、3、4季度的排名,然后计算四季度排名差,保存最大排名差。
如某商品PAPER CHAIN KIT VINTAGE CHRISTMAS (复古圣诞纸链套装)的交易量No_Of_Trans在第1、2、3、4季度的排名分别为1669、1734、110、4,
值(交易量)分别为:11、9、143、509,因此该商品的排名差为:1734-4=1730。排名差越大,说明是季节性商品的可能性越大。
5.对交易订单量、销售量、销售额3个维度的商品排名差进行降序排序,选取排名差最大的3个商品。
6.分别绘制交易订单量、销售量、销售额3个维度排名差最大的3个商品四个季度的对应值的饼图。
"""
# 1.选取英国2011年的数据,将1-12月转换为1-4季度。
df_UK = data.filter((col("Country") == "United Kingdom") & (col("Year") == 2011)) # 只选取UK英国2011年的全年数据
df_UK = df_UK.withColumn("Season", floor((col("Month") - 1) / 3) + 1) # 将1-12月转换为相应的1-4季度,新增一列:季度
# 分组与聚合:按季度、商品名称分组,统计季度的成交量、销售量、销售额
df_UK_seasonal = df_UK.groupby(["Season", "Itemname"]).agg(
countDistinct("BillNo").alias("No_Of_Trans"), # 成交量
sum("Quantity").alias("Quantity"), # 销售量
sum("Total_Price").alias("Total_Price") # 销售额
)
print("\n按季度、商品名称分组,统计季度的成交量、销售量、销售额:\n")
df_UK_seasonal.orderBy('Itemname').show(20, False)
print('df_UK_seasonal的行数:', df_UK_seasonal.count())
df_UK_seasonal = df_UK_seasonal.toPandas()
# 排序
# 2.确定每季度每件商品的交易订单量、销售量、销售额排名,并添加列给每件商品标注排名。
def rank_seasonal(df, season="all"):
groupby_cols = ["Itemname"]
if season != "all":
groupby_cols.append("Season")
df = df[df["Season"] == int(season)] # 提取某一个季节的数据
df = df.groupby(groupby_cols).sum(
["No_Of_Trans", "Quantity", "Total_Price"]) # 按商品名分组,再按季节,计算No_Of_Trans,Quantity,Total_Price的总和
for col in ["No_Of_Trans", "Quantity", "Total_Price"]:
df[col + "_Rank"] = df[col].rank(ascending=False) # 确定每件商品的交易数,销售量,销售额的排名,并添加相应的排名列
return df
df_UK_all = rank_seasonal(df_UK_seasonal)
df_UK_first = rank_seasonal(df_UK_seasonal, "1")
df_UK_second = rank_seasonal(df_UK_seasonal, "2")
df_UK_third = rank_seasonal(df_UK_seasonal, "3")
df_UK_fourth = rank_seasonal(df_UK_seasonal, "4")
print(df_UK_all)
print(df_UK_fourth)
"""
在Apache Spark的PySpark模块中,Window函数是一个非常强大的工具,它允许你在保持数据集为分布式的同时,执行复杂的分组聚合操作,
这些操作类似于SQL中的窗口函数(Window Functions)或分析函数(Analytic Functions)。Window函数不是直接对数据集进行分区,
而是定义了一个“窗口”或“框架”,在这个窗口内可以对数据进行聚合或排序等操作,而不需要将数据实际地分组到一起。
partitionBy:分组,
"""
# 全年交易量、销售量、销售额排名
# 3.选取四季度交易订单量、销售量、销售额3个维度排名的商品,四季度前10商品名去重后按照维度一起存放。此时,存放的每个维度的商品数量(集合)有可能大于10件。
dfs = [df_UK_all, df_UK_first, df_UK_second, df_UK_third, df_UK_fourth]
item_set_No_Of_Trans = set() # 如果 DataFrame 中的有关列 包含重复值,并且这些值在排名前十的列表中,则它们在集合中只会出现一次。这是集合(set)的特性,它不允许重复元素。
item_set_Quantity = set()
item_set_Total_Price = set()
for df in dfs: # 遍历全年及四个季度的交易量、销售量、销售额排行榜,提取排行前十的商品名字
df = df.copy().reset_index()
itemname_top_Trans = df[df["No_Of_Trans_Rank"] <= 10]["Itemname"] # 交易量排名前十的商品名字
itemname_top_Qty = df[df["Quantity_Rank"] <= 10]["Itemname"] # 销售量排名前十的商品名字
itemname_top_TTL_PRICE = df[df["Total_Price_Rank"] <= 10]["Itemname"] # 销售额排名前十的商品名字
for i in itemname_top_Trans:
item_set_No_Of_Trans.add(i)
for i in itemname_top_Qty:
item_set_Quantity.add(i)
for i in itemname_top_TTL_PRICE:
item_set_Total_Price.add(i)
print("全年及四个季度排名前10的商品的集合(商品不重复):\n")
pd.set_option('display.max_columns', None) # 显示所有列
print(item_set_No_Of_Trans) # 全年及四季度交易订单量排行前十的商品的集合
print(item_set_Quantity) # 全年及四季度销售量排行前十的商品的集合
print(item_set_Total_Price) # 全年及四季度销售额排行前十的商品的集合
## 每个季度的是否有所不同?
"""
4.是否存在季节性商品,也就是分析四季度排名前10的商品集合,分别在1、2、3、4季度的排名,然后计算四季度排名差,保存最大排名差。
如某商品PAPER CHAIN KIT VINTAGE CHRISTMAS (复古圣诞纸链套装)的交易量No_Of_Trans在第1、2、3、4季度的排名分别为1669、1734、110、4,
值(交易量)分别为:11、9、143、509,因此该商品的排名差为:1734-4=1730。排名差越大,说明是季节性商品的可能性越大。
"""
RANK_DF = {"Dimension": [], "item": [], "Rank": [], "Value": [], "Season": [],
"Range": []} # pd.DataFrame(columns=["Dimension","item","Rank","Value"])
for dim, itemset in [("No_Of_Trans", item_set_No_Of_Trans),
("Quantity", item_set_Quantity),
("Total_Price", item_set_Total_Price)]: # 3个维度dim:No_Of_Trans,Quantity,Total_Price
for i in list(itemset): # 遍历 全年及四季度排名前十的商品名,逐一取出商品名给i
min_rank = 9999
max_rank = -1
for j, df in enumerate(dfs): # 遍历全年及四季度交易订单量,销售量,销售额的排名数据,j是dfs的索引。
df = df.reset_index() # 重置索引,索引号为原来的自然编号
if j == 0: # j=0,表示取出的是全年订单量,销售量,销售额的排名数据
RANK_DF["Season"].append("all")
else:
RANK_DF["Season"].append(df["Season"].values[0])
sub_df = df[df["Itemname"] == i] # 比对排名前十商品名,抽取该商品数据信息(包含3项排名信息)
if sub_df.shape[0] == 0:
curr_rank = 9999
curr_value = 0
else:
curr_rank = df[df["Itemname"] == i][dim + "_Rank"].values[0] # 该商品在该季度对应维度的排名
curr_value = df[df["Itemname"] == i][dim].values[0] # 提取该商品对应维度的数据,如该商品交易量是多少。
min_rank = __builtins__.min(curr_rank,
min_rank) # 商品的当前排名curr_rank和最小排名min_rank比较,更新min_rank。使用python内置的min函数,而不是pyspark。
max_rank = __builtins__.max(curr_rank, max_rank) if curr_rank < 9999 else max_rank
RANK_DF["Dimension"].append(dim) # 用字典为该商品创建相关信息,包括维度,商品名,排名,该商品在该维度的数量
RANK_DF["item"].append(i)
RANK_DF["Rank"].append(curr_rank)
RANK_DF["Value"].append(curr_value)
RANK_DF["Range"] += [max_rank - min_rank] * 5 # 将该排名差放在该商品的全年及四季度的Range列里面。每件商品出现5次,对应全年及四季度的数据
RANK_DF = pd.DataFrame(RANK_DF) # 将字典转成pandas的dataframe
print(RANK_DF)
# “每个季度的量相差很多”的,但是本身很多的
"""
5.对交易订单量、销售量、销售额3个维度的商品排名差进行降序排序,选取排名差最大的3个商品。
6.分别绘制交易订单量、销售量、销售额3个维度排名差最大的3个商品四个季度的对应值的饼图。
"""
plt.figure(figsize=(20, 20))
dims = ["No_Of_Trans", "Quantity", "Total_Price"]
for i, dim in enumerate(dims): # 遍历列表,取索引号和元素
tmp_df = RANK_DF[(RANK_DF["Dimension"] == dim) & (RANK_DF["Season"] != 'all')] # 取出对应维度的四季度数据,剔除全年(all)数据。
tmp_df["Range_Rank"] = tmp_df["Range"].rank(ascending=False) # 对排名差 进行降序排序,并添加一列数据Range_Rank
tmp_df = tmp_df.sort_values("Range_Rank").reset_index(drop=True) # 重置索引,且不在新的DataFrame中保留旧的索引作为一列
for j in range(3): # 提取每个维度的前j=3个商品,绘制每个季度对应维度(如No_Of_Trans交易量)数据的饼图。
tmp_df_rank = tmp_df.loc[4 * j:4 * (j + 1) - 1, :] # 分别提取前3个商品的4行数据(四季度)
this_item = list(set(tmp_df_rank["item"]))[0] # 从tmp_df_rank的"item"列中选取一个唯一的(不重复的)元素,集合set自动过滤重复值
plt.subplot(3, 3, i * 3 + j + 1)
plt.pie(tmp_df_rank["Value"], labels=tmp_df_rank["Season"]) # 绘制四季度相应维度的数据
plt.title(f"{dim}:{this_item}")
plt.show()
"""
#上图中的商品,是在全年的销售中名销售额/销售了/成交量列前茅的前3个商品,但是从饼图上来看,这些商品明显是会集中在某一个季度中畅销。
把颗粒度缩小到按月份来看,计算每月销售额占总销售额的比重(季节指数);并以此排序看看哪些商品的季节指数极差最大。
"""
选取排名差最大的前3个商品,计算交易量、成交量和成交额在四季度中所占的比例。绘制的饼图如下:
上图中,
第一行3个饼图,表示四个季度中成交量排名相差最大的前3个商品,在四个季度中的交易量占比。如商品“PAPER CHAIN KIT VINTAGE CHRISTMAS(复古圣诞纸链套装)”是季节性商品,在第4季度的交易量占比最大。
第二行的3个饼图,表示四个季度中成交量排名相差最大的前3个商品,在四个季度中的成交量占比。
第三行的3个饼图,表示四个季度中成交额排名相差最大的前3个商品,在四个季度中的成交额占比。
2.3.5 哪些商品的季节指数极差最大(按月判断)
上图中的商品,是按季度划分,交易量、成交量、成交额名列前茅的前3个商品,从饼图上来看,这些商品明显集中在某一个季度中畅销。如果按月分组,哪些商品季节指数极差最大?把数据分析颗粒度缩小到按月份分组,计算每月交易量、成交量、成交额/全年月平均交易量、成交量、成交额,即:季节指数,并以此排序看看哪些商品的季节指数极差最大。
代码如下:
# ------------(五)哪些商品的季节指数极差最大(按月判断)---------------------
# 把颗粒度缩小到按月份来看,计算每月交易量/全年月平均交易量的(季节指数);并以此排序看看哪些商品的季节指数极差最大。以此类推,再计算成交量、成交额的季节指数
from pyspark.sql import functions as F
df_UK_monthly = df_UK.groupby(["Month", "Itemname"]).agg(F.countDistinct(F.col("BillNo")).alias("No_Of_Trans"),
F.sum(F.col("Quantity")).alias("Quantity")
, F.sum(F.col("Total_Price")).alias(
"Total_Price")).toPandas().reset_index(drop=True) # 每月每件商品的交易量、成交量、成交额总数
df_UK_fullyear = df_UK_monthly.groupby(["Itemname"]).mean(
["No_Of_Trans", "Quantity", "Total_Price"]).reset_index() # 每件商品交易量、成交量、成交额的全年月均值
df_UK_fullyear.rename(
columns={"No_Of_Trans": "FY_No_Of_Trans", "Quantity": "FY_Quantity", "Total_Price": "FY_Total_Price"}, inplace=True)
df_UK_Monthly_season = pd.merge(df_UK_monthly, df_UK_fullyear, on=["Itemname"], how='left')
df_UK_Monthly_season["seasonal_index_No_Of_Trans"] = df_UK_Monthly_season["No_Of_Trans"] / df_UK_Monthly_season[
"FY_No_Of_Trans"]
df_UK_Monthly_season["seasonal_index_Quantity"] = df_UK_Monthly_season["Quantity"] / df_UK_Monthly_season["FY_Quantity"]
df_UK_Monthly_season["seasonal_index_Total_Price"] = df_UK_Monthly_season["Total_Price"] / df_UK_Monthly_season[
"FY_Total_Price"] # 每月成交额/全年月平均成交额
# 算极差
# 每月交易量/全年月平均交易量(季节指数),看看哪些商品的季节指数极差最大。以此类推,再计算成交量、成交额的季节指数和极差
df_UK_season_max = df_UK_Monthly_season.groupby("Itemname").max(
["seasonal_index_No_Of_Trans", "seasonal_index_Quantity", "seasonal_index_Total_Price"]).reset_index().rename(
columns={"seasonal_index_No_Of_Trans": "seasonal_index_No_Of_Trans_max",
"seasonal_index_Quantity": "seasonal_index_Quantity_max",
"seasonal_index_Total_Price": "seasonal_index_Total_Price_max"}) # 计算每样商品的交易量、成交量、成交额季节指数最大值。
df_UK_season_min = df_UK_Monthly_season.groupby("Itemname").min(
["seasonal_index_No_Of_Trans", "seasonal_index_Quantity", "seasonal_index_Total_Price"]).reset_index().rename(
columns={"seasonal_index_No_Of_Trans": "seasonal_index_No_Of_Trans_min",
"seasonal_index_Quantity": "seasonal_index_Quantity_min",
"seasonal_index_Total_Price": "seasonal_index_Total_Price_min"}) # 每样商品的交易量、成交量、成交额,那个月的季节指数最小。
df_UK_season_ranges = pd.merge(df_UK_season_min, df_UK_season_max, on=["Itemname"])
seasonal_index_cols = ["seasonal_index_No_Of_Trans", "seasonal_index_Quantity", "seasonal_index_Total_Price"]
for col in seasonal_index_cols:
df_UK_season_ranges[col] = df_UK_season_ranges[col + "_max"] - df_UK_season_ranges[col + "_min"]
df_UK_season_ranges[col + "_Rank"] = df_UK_season_ranges[col].rank(ascending=False)
df_UK_season_ranges = df_UK_season_ranges[
["Itemname"] + [i for i in df_UK_season_ranges if "seasonal_index" in i]] # 选取Itemname列和含seasonal_index的列。
# -------------月度极差可视化(每个维度排名前3的商品)-------------
# 提取月度极差排名前3的商品,绘制其1-12月相应维度数据的折线图
plt.figure(figsize=(15, 15))
for i, col in enumerate(["seasonal_index_No_Of_Trans", "seasonal_index_Quantity", "seasonal_index_Total_Price"]):
seasonal_items = df_UK_season_ranges.sort_values(col + "_Rank").reset_index().loc[:2,
"Itemname"].tolist() # 选择相应维度的极差指数的前3名 商品
for j, item in enumerate(seasonal_items):
plt.subplot(3, 3, i * 3 + j + 1)
idxs = [k for k in df_UK_monthly.index if df_UK_monthly.loc[k, "Itemname"] == item] # df_UK_monthly查找相应物品的索引
tmp_df = df_UK_monthly.loc[idxs, ["Itemname", "Month", col.replace("seasonal_index_","")]]
# 根据这些索引,从df_UK_monthly中选取对应的商品名称、月份和相应的交易量/销售量/销售额
tmp_df = tmp_df.pivot(index="Month", columns="Itemname",
values=col.replace("seasonal_index_", ""))
# 将tmp_df数据透视,以月份为行索引,商品名称为列索引,交易量/销售量/销售额为数据
plt.plot(tmp_df, marker='o')
plt.ylabel(col.replace("seasonal_index_", ""))
plt.title(item)
plt.subplots_adjust(wspace=0.2, hspace=0.2) # 调整子图间距。wspace=0.2,水平间距为 整图的20%,hspace=0.2,垂直方向
plt.show()
"""
此处分别从交易量、成交量、成交额3个维度构造了季节指数,并将极差排名前3的商品作了折线统计图。
从图中来看,这些商品全都是“集中在某一个月份十分畅销,但是在其他月份相对无人问津”。
"""
# -----------将全年及每个季度排名top10的商品的交易量、成交量、成交额作水平柱形图,图中商品旁边标出了它对应交易量/成交量/成交额的极差排名--------
# 对RANK_DF按Dimension分组,并取每个小组的第一个非空行,然后再选择"Dimension", "item", "Range"三列数据。
ranges = RANK_DF.groupby(["Dimension", "item"]).first().reset_index()[["Dimension", "item", "Range"]]
## No_Of_Trans交易量排名前十商品,及商品相应的极差
plt.figure(figsize=(20, 20))
range_df = ranges[ranges["Dimension"] == "No_Of_Trans"]
titles = ["All", "Season One", "Season Two", "Season Three", "Season Four"]
for n in range(5):
plt.subplot(5, 1, n + 1)
df = dfs[n].copy().reset_index() # 选取全年或1、2、3、4季度的数据
# [i for i in df.index if df.loc[i, "Itemname"] in item_set_No_Of_Trans] 选取在排名前10商品表item_set_No_Of_Trans
# 中的df的行索引,再选取df的["Itemname", "No_Of_Trans"]两列。
df = df.loc[[i for i in df.index if df.loc[i, "Itemname"] in item_set_No_Of_Trans], ["Itemname", "No_Of_Trans"]]
df = df.sort_values("No_Of_Trans", ascending=False)
df = df.iloc[:10, :] # 选取 交易量前10 商品。
df = pd.merge(df, range_df, left_on="Itemname", right_on="item", how="left") # 两表左合并,使整表的商品名Itemname和Range列。
# df["Itemname"] + " " + df["Range"].astype("str")条形图的标签,df["No_Of_Trans"]条形图长度
plt.barh(df["Itemname"] + " " + df["Range"].astype("str"), df["No_Of_Trans"])
plt.title("No_Of_Trans " + titles[n])
plt.subplots_adjust(hspace=0.4) # 调整子图间距。wspace=0.2,水平间距为 整图的20%,
plt.show()
# Quantity成交量排名前十商品,及商品相应的极差
plt.figure(figsize=(20, 20))
range_df = ranges[ranges["Dimension"] == "Quantity"]
titles = ["All", "Season One", "Season Two", "Season Three", "Season Four"]
for n in range(5):
plt.subplot(5, 1, n + 1)
df = dfs[n].copy().reset_index()
df = df.loc[[i for i in df.index if df.loc[i, "Itemname"] in item_set_Quantity], ["Itemname", "Quantity"]]
df = df.sort_values("Quantity", ascending=False)
df = df.iloc[:10, :]
df = pd.merge(df, range_df, left_on="Itemname", right_on="item", how="left")
plt.barh(df["Itemname"] + " " + df["Range"].astype("str"), df["Quantity"])
plt.title("Quantity " + titles[n])
plt.subplots_adjust(hspace=0.4) # 调整子图间距。wspace=0.2,水平间距为 整图的20%,
plt.show()
# Total_Price成交额排名前十商品,及商品相应的极差
plt.figure(figsize=(20, 20))
range_df = ranges[ranges["Dimension"] == "Total_Price"]
titles = ["All", "Season One", "Season Two", "Season Three", "Season Four"]
for n in range(5):
plt.subplot(5, 1, n + 1)
df = dfs[n].copy().reset_index()
df = df.loc[[i for i in df.index if df.loc[i, "Itemname"] in item_set_Total_Price], ["Itemname", "Total_Price"]]
df = df.sort_values("Total_Price", ascending=False)
df = df.iloc[:10, :]
df = pd.merge(df, range_df, left_on="Itemname", right_on="item", how="left")
plt.barh(df["Itemname"] + " " + df["Range"].astype("str"), df["Total_Price"])
plt.title("Total_Price " + titles[n])
plt.subplots_adjust(hspace=0.4) # 调整子图间距。wspace=0.2,水平间距为 整图的20%,
plt.show()
月度极差排名前3的商品,绘制其1-12月相应维度数据的折线图,如下图所示。
全年及每个季度交易量、成交量、成交额排名top10的商品的,图中商品旁边标出了它对应交易量、成交量、成交额的极差排名。交易量的水平柱形图如下所示。
成交量的柱形图如下所示:
成交额的柱形图如下所示:
可以看到,虽然大多数在全年销售中获得的top10排名极差都较小(总共有3000+个商品), 但是在总成交额的全年top10中,依然有一个明显存在季节性效应的商品(PAPER CHAIN KIT 50’s CHRISMAS)。
2.3.6 同一个商品在不同维度上全年排名之间的排名差
计算每件商品在交易量、成交量、成交额的排名,并将这3个维度排名之差最大的5个商品列出。
代码如下:
# --------(六)同一个商品在不同维度上的全年排名是否排名差距较大---------
"""
计算了每件商品在交易量、成交量、成交额的全年排名,并将这3个维度排名之差最大的5个商品列出了。
"""
df_UK_all = df_UK_monthly.groupby(["Itemname"]).sum(["No_Of_Trans", "Quantity", "Total_Price"]).reset_index()
df_UK_all["No_Of_Trans_Rank"] = df_UK_all["No_Of_Trans"].rank(ascending=False) # 给交易量标注排名(降序),并添加排名列
df_UK_all["Quantity_Rank"] = df_UK_all["Quantity"].rank(ascending=False)
df_UK_all["Total_Price_Rank"] = df_UK_all["Total_Price"].rank(ascending=False)
# df_UK_all.shape[0])表示行数。遍历df_UK_all所有行,取出交易量、成交量、成交额的排名,计算这3个数的最大最小值。
# 再计算最大最小值之差Rank_Gaps,即同一商品的排名差。
Rank_Gaps = [
__builtins__.max(df_UK_all.loc[i, "No_Of_Trans_Rank"], df_UK_all.loc[i, "Quantity_Rank"],
df_UK_all.loc[i, "Total_Price_Rank"]) \
- __builtins__.min(df_UK_all.loc[i, "No_Of_Trans_Rank"], df_UK_all.loc[i, "Quantity_Rank"],
df_UK_all.loc[i, "Total_Price_Rank"]) \
for i in range(df_UK_all.shape[0])]
df_UK_all["Rank_Gaps"] = Rank_Gaps #
df_ranked_all = df_UK_all.sort_values("Rank_Gaps", ascending=False).reset_index(drop=True) # 最大最小值之差 降序排序
# --------------------------可视化:在交易量、成交量、成交额全年排名相差最大的5个商品----------------------
plt.figure(figsize=(20, 20))
for i in range(5): # 取5个商品
plt.subplot(5, 1, i + 1)
sub = df_ranked_all.iloc[i, :] # 逐一取出5行数据(5个商品的数据)
item_name = sub["Itemname"]
sub = dict(sub) # 将pandas的dataframe做成字典。方便sub["No_Of_Trans_Rank"]等取值。。
sub_df = pd.DataFrame({"index": ["No_Of_Trans_Rank", "Quantity_Rank", "Total_Price_Rank"],
"Value": [sub["No_Of_Trans_Rank"], sub["Quantity_Rank"], sub["Total_Price_Rank"]]})
plt.barh(sub_df["index"], sub_df["Value"])
plt.title(item_name)
plt.subplots_adjust(hspace=0.4) # 调整子图间距。wspace=0.2,水平间距为 整图的20%,
plt.show()
本商品全年交易量、成交量、成交额排名之差最大的前5个商品,如下图所示。
可以发现上图中的商品,有些虽然成交额与成交量名列前茅,但是成交量却排名靠后。特别是第一名(PAPER CRAFT, LITTLE BRIDE),实际看表格数据时会发现它只有1个单子,但是那1个单子却订了8万多个这个商品。
2.3.7 探索客单价、客单量与平均售价
构造以下3个KPI(每月):
客单量(UPT, Units Per Transaction)= 成交量(Quantity)/交易量(No_Of_Trans), 客单量是指平均每笔交易所包含的商品数量。
平均售价(ASP, Average Selling Price)= 成交额(Total_Price)/ 成交量(Quantity),平均售价是指售出商品的平均价格。
客单价(ATV, Average Transaction Value)= 成交额(Total_Price)/ 成交量(No_Of_Trans),客单价是指平均每笔交易的总金额。
客单量,平均每笔交易所包含的商品数量,如下图所示。
平均售价,售出商品的平均价格,如下图所示。
客单价,平均每笔交易的总金额,如下图所示。
每月交易量,如下图所示。
11月的高成交额实际上是由于11月的高交易量。尽管12月份的成交额再次下降,但12月份的订单大多都是大单子。
3 数据挖掘—关联规则分析
关联规则分析为的就是发现商品与商品之间的关联。通过计算商品之间的支持度、置信度与提升度,分析哪些商品有正向关系,顾客愿意同时购买它们。
此处使用了pyspark自带的FPGrowth算法。它和APRIORI算法一样都是计算两两商品之间支持度置信度与提升度的算法,虽然算法流程不同,但是计算结果是一样的。
关联规则算法只需要每个订单商品名称,不需要其他数据。因此,每个订单的商品名称放在一个列表里,整理后的 数据集如下图所示。
利用Pyspark机器学习库的关联规则模型,对上述数据集进行关联分析,代码如下:
from pyspark.ml.fpm import FPGrowth, FPGrowthModel
"""
df_UK_concatenated = df_UK\
.groupby(["BillNo", "Itemname"])\
.agg(F.sum("Quantity").alias("Quantity"))\
.groupby("BillNo")\
.agg(F.collect_list(F.col("Itemname")).alias("items"))
"""
# 将同一个订单号的商品集合到一个列表。
df_UK_concatenated = df_UK \
.groupby(["BillNo", "Itemname"]) \
.agg(F.sum("Quantity").alias("Quantity")) # 按"BillNo", "Itemname"分组,计算Quantity的总数(分组后必须要有一个运算式子)
print("关联规则,按BillNo, Itemname分组:")
df_UK_concatenated.show(10, False)
df_UK_concatenated = df_UK_concatenated \
.groupby("BillNo") \
.agg(F.collect_list(F.col("Itemname")).alias("items"))
print("关联规则,按BillNo分组,将分组内的Itemname列的值,收集到列表items:")
df_UK_concatenated.show(10)
model = FPGrowth(minSupport=0.03, minConfidence=0.3)
model = model.fit(df_UK_concatenated.select("items"))
res = model.associationRules.toPandas()
# -----------------------关联规则可视化-----------------------
combs = [('confidence', 'lift', 'support'), ('lift', 'support', 'confidence'), ('support', 'confidence', 'lift')]
for i, (x, y, c) in enumerate(combs):
plt.subplot(3, 1, i + 1)
sc = plt.scatter(res[x], res[y], c=res[c], cmap='viridis')
plt.xlabel(x)
plt.ylabel(y)
plt.colorbar(sc, label=c)
plt.show()
# 支持度(support)前三的商品组合(每2条实际上是同一组商品)。
print(res.sort_values("support", ascending=False).head(6))
# 置信度(confidence)前5的商品组合,置信度指的是当商品A被购买时,商品B也被购买的概率。
print(res.sort_values("confidence", ascending=False).head(5))
# 提升度(lift)前三的商品组合(每2条实际上是同一组商品)。 提升度可以看做是2件商品之间是否存在正向/反向的关系。
print(res.sort_values("lift", ascending=False).head(6))
(1) Antecedent(前件)
定义:在关联规则中,if部分称为前提(Antecedent),表示某些物品的组合。它是规则中用于推断或预测的条件部分。
示例:在规则“购买牛奶 → 购买面包”中,“购买牛奶”是前件。
(2) Consequent(后件)
定义:在关联规则中,then部分称为结果(Consequent),表示其他物品的组合。它是在前件满足时可能发生的结果。
示例:在规则“购买牛奶 → 购买面包”中,“购买面包”是后件。
(3) Confidence(置信度)
定义:置信度是关联规则的可信程度,表示在包含前件的事务中,同时也包含后件的事务所占的比例。
示例:如果购买牛奶的顾客中有70%也购买了面包,则规则“购买牛奶 → 购买面包”的置信度为70%。
(4) Lift(提升度)
定义:提升度表示在包含前件的事务中同时包含后件的比例,与仅包含后件的事务的比例之间的比值。它反映了前件对后件出现的提升作用。
示例:如果购买牛奶的顾客中购买面包的比例是70%,而所有顾客中购买面包的比例是50%,则规则“购买牛奶 → 购买面包”的提升度为1.4(70% / 50%)。
(5) Support(支持度)
定义:支持度是项集在数据集中出现的频率或概率,表示同时包含前件和后件的事务占所有事务的比例。
示例:如果100个购物交易中,有30个交易同时包含了牛奶和面包,则规则“购买牛奶 → 购买面包”的支持度为30%。
按支持度(support)降序排序的商品组合(每2条实际上是同一组商品)。
第一条关联规则:[‘JUMBO BAG PINK POLKADOT’]----->[‘JUMBO BAG RED RETROSPOT’],
置信度confidence为0.67541,表示购买了JUMBO BAG PINK POLKADOT商品的顾客,有67.541%的概率会购买商品JUMBO BAG RED RETROSPOT。
提升度lift为6.10348,如果该值大于1,则说明该关联规则有效,值越大,有效性越强。
支持度support为0.04534,表示有4.5%的交易包含了商品JUMBO BAG PINK POLKADOT和商品JUMBO BAG RED RETROSPOT,即这两样商品在所有交易中出现的频率。
置信度(confidence)降序排名的商品组合,置信度指的是当商品A被购买时,商品B也被购买的概率。
关联规则:[‘PINK REGENCY TEACUP AND SAUCER’]---->[‘GREEN REGENCY TEACUP AND SAUCER’]的置信度confidence为0.81974,提升度lift为15.50452,支持度support为0.03478。
提升度(lift)降序排名的商品组合(每2条实际上是同一组商品)。 提升度可以看做是2件商品之间是否存在正向/反向的关系。
[‘PINK REGENCY TEACUP AND SAUCER’]—>[‘GREEN REGENCY TEACUP AND SAUCER’]的置信度confidence为0.81974,提升度lift为15.50452,支持度support为0.03478。
可将关联规则模型推荐的结果存储在MySQL数据库,再通过Flask在可视化大屏展示。
4 数据可视化大屏的设计
这部分内容可参考作者写的另外一篇文章,Flask+Pyecharts+大数据集群:数据可视化大屏的实现:链接: https://blog.csdn.net/chendengyi2/article/details/1399371066,“六、Flask+Pyecharts+大数据集群(Linux):绘制数据可视化大屏。”
前期准备:安装PyEchats,Flask等组件。
Flask:是一个轻量级、灵活、可扩展的Web应用框架,使用Python编写,适合用于构建中小型Web应用程序。它提供了基本的路由、模板引擎、URL构建、错误处理等功能,并支持插件和扩展来增强其功能。
Flask提供了一个路由系统,可以将不同的URL路径映射到相应的处理函数上。当用户访问特定的URL时,Flask会调用相应的处理函数,并返回响应。
Flask内置了一个基于Jinja2的模板引擎,开发者可以使用模板来渲染HTML页面。通过将动态数据传递给模板,并使用模板语言来定义页面的结构和样式,开发者可以轻松地创建和管理Web页面的外观和布局。
Pyecharts:是一款基于Python的开源数据可视化库,它整合了Echarts与Python的优势,使得在Python环境中能够轻松创建出美观且交互性强的图表。Pyecharts支持多达数十种图表类型,包括折线图、柱状图、散点图、饼图等常见图表,以及地图、热力图、关系图等特色图表。这些丰富的图表类型能够满足不同场景下的数据可视化需求。
Pyecharts的基本教程可以参考本文作者的另外一篇文章:Pyecharts快速入门及高清图片保存:链接: https://blog.csdn.net/chendengyi2/article/details/139878882
在IDEA项目上创建3个目录:data,static,templates,和1个python文件:main.py。data用来存储本地数据,static用来存放Echarts的图表和组件模板文件,templates用来存放html文件,项目的目录结构如下图所示。
在templates文件上单击鼠标右键,将该目录标记为模板文件夹,Flask默认渲染该文件下的HTML文件。
到Echarts官网下载模板和组件json文件:链接: https://echarts.apache.org/zh/download.html
将下载的echats.js文件复制到static目录。
在templates目录下新建html文件,用于展示可视化图形,选择HTML 5文件格式,命名为show_pyecharts_05。
打开main.py文件,清空里面的内容,输入以下代码:
import json
import pyecharts.charts
from flask import Flask, render_template
from pyecharts.options import *
from markupsafe import Markup # 导入 Markup,用于在 Flask 模板中安全地渲染 HTML
from pyecharts import charts
import pandas as pd
from sqlalchemy import create_engine
from pyecharts.charts import Pie, Bar, Line
from pyecharts.options import LabelOpts, TitleOpts, LegendOpts
import pymysql
from sqlalchemy import create_engine
app = Flask(__name__) # 创建一个 Flask 应用实例
# ---------------4.读取集群的MySQL数据,绘制可视化大屏------------------------
# 自定义函数,读master节点的MySQL数据
def connFun_linux(sql_query):
engine = create_engine("mysql+pymysql://root:123456@192.168.126.10:3306/test")
try:
data = pd.read_sql(sql_query, engine)
return data
except Exception as e:
print(f"An error occurred: {e}")
raise # 可选:重新抛出异常以便在外部捕获 '''
# 读取master节点(Linux系统)中的MySQL数据,自定义绘图函数
def get_pie_linux():
sql = '''select * from data_ttl_country'''
data = connFun_linux(sql) # 确保这个函数返回的是DataFrame
# data.set_index("Country", inplace=True)
top_five = data.sort_values("Quantity", ascending=False)[:5]
others_quantity = data.sort_values("Quantity", ascending=False)[5:]['Quantity'].sum() # 其余国家的销售量和销售额总和
others_total_price = data.sort_values("Quantity", ascending=False)[5:]['Total_Price'].sum() # 其余国家的销售量和销售额总和
top_five2 = top_five.copy()
others_df = pd.DataFrame(
{"Country": ["others"], "Quantity": [others_quantity], "Total_Price": [others_total_price]})
pie_data = pd.concat([top_five2, others_df], ignore_index=True)
country = list(pie_data["Country"])
quantity = list(pie_data['Quantity'])
data_list = [list(z) for z in zip(country, quantity)]
c = (
Pie()
.add("", data_list, radius=["40%", "75%"])
.set_global_opts(title_opts=TitleOpts(title="成交量"),
legend_opts=LegendOpts(orient="vertical", pos_top="15%", pos_left="2%"))
.set_series_opts(label_opts=LabelOpts(formatter="{b}: {c}"))
)
return c
def get_bar_linux():
sql = '''select * from data_ttl_weekday '''
data = connFun_linux(sql)
data = data.sort_values('DayOfWeek', ascending=True)
# x = list(data["DayOfWeek"])
x = ['星期一', '星期二', '星期三', '星期四', '星期五', '星期六', '星期日']
y1 = list(data['Quantity'])
c = (
pyecharts.charts.Bar()
.add_xaxis(x)
.add_yaxis("成交量(Quantity)", y1)
.set_global_opts(title_opts=TitleOpts(title="周一到周日成交量", subtitle="bar"),
xaxis_opts=AxisOpts(axislabel_opts=LabelOpts(rotate=45)), )
)
return c
def get_line_linux():
sql = '''select * from data_ttl_time '''
data = connFun_linux(sql)
data = data.sort_values(['Year', 'Month'])
x = [str(i) + "\n" + str(j) for i, j in zip(data["Year"], data["Month"])] # x轴刻度标签
y2 = list(data["Quantity"])
y1 = list(data['Total_Price'].round(0)) # 保留2位小数
c = (
Line()
.add_xaxis(x)
.add_yaxis("成交额", y1,
markline_opts=MarkLineOpts(data=[MarkLineItem(type_="average")]),
label_opts=LabelOpts(is_show=False), ) # 不显示数据点标签
.add_yaxis("成交量", y2,
markline_opts=MarkLineOpts(data=[MarkLineItem(type_="average")]),
label_opts=LabelOpts(is_show=False),
markpoint_opts=MarkPointOpts(data=[MarkPointItem(coord=[11, y2[11]], value=y2[11])]), )
.set_global_opts(title_opts=TitleOpts(title="成交量和成交额折线图"))
)
return c
def get_barstack_linux():
sql = '''select * from data_ttl_country_time '''
data_ttl_country_time = connFun_linux(sql)
data_ttl_country_time["isUnitedKingdom"] = ["UK" if i == "United Kingdom" else "Not UK" for i in
data_ttl_country_time["Country"]] # 创建isUnitedKingdom列
data_ttl_country_time_isUK = data_ttl_country_time.groupby(["isUnitedKingdom", "Year", "Month"])[
"Total_Price"].sum().reset_index() # 分组并计算成交额
data_ttl_country_time_isUK.index = [str(i) + "\n" + str(j) for i, j in
zip(data_ttl_country_time_isUK["Year"],
data_ttl_country_time_isUK["Month"])] # 重建索引
UK = data_ttl_country_time_isUK[data_ttl_country_time_isUK["isUnitedKingdom"] == "UK"] # 英国UK的每年每月成交额
NUK = data_ttl_country_time_isUK[data_ttl_country_time_isUK["isUnitedKingdom"] == "Not UK"] # 非英国UK的每年每月成交额
x = list(UK.index)
y1 = list(UK['Total_Price'])
y2 = list(NUK['Total_Price'])
c = (
Bar()
.add_xaxis(x)
.add_yaxis("英国", y1, stack="stack1")
.add_yaxis("其他国家", y2, stack="stack1")
.set_series_opts(label_opts=LabelOpts(is_show=False))
.set_global_opts(title_opts=TitleOpts(title="成交额"), toolbox_opts=ToolboxOpts(is_show=True), )
)
return c
@app.route('/show_pyecharts_05')
def show_pyecharts_05():
pie = get_pie_linux()
bar = get_bar_linux()
line = get_line_linux()
barstack = get_barstack_linux()
return render_template("show_pyecharts_05.html",
pie_options=pie.dump_options(),
bar_options=bar.dump_options(),
line_options=line.dump_options(),
barstack_options=barstack.dump_options(), )
if __name__ == "__main__":
app.run(host='127.0.0.1', port=5000, debug=True)
上面的程序,将通过Pandas访问MySQL,读取PySpark存放的数据,读取MySQL数据库的代码如下图所示:
图中,192.168.126.10为大数据集群主节点Master的IP地址,3306为MySQL的访问端口,test为MySQL数据库的名字。root为MySQL用户名,123456为密码。
这部分的编程流程是:先用Pandas读取MySQL数据,PyEcharts绘制图表,Flask再将PyEchart绘制的图表渲染到show_pyecharts_05.html页面。
打开show_pyecharts_05.html,清空里面的内容,粘贴以下代码:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>ECharts</title>
<style >
/* CSS 样式将放在这里 */
.chart-container{
display:flex;
flex-direction:column; /* 默认为column,表示垂直布局 */
justify-content:center; /* 水平居中 */
align-items:center; /* 垂直居中(如果需要的话) */
height:150vh; /* 根据需要设置容器高度 */
width:100%; /* 占据全屏宽度 */
}
.chart-row{
display: flex; /* 每一行都是一个flex容器 */
justify-content:space-between; /* 列之间均匀分布 */
margin-bottom:20px; /* 行与行之间的间距 */
}
.chart-item{
flex: 1; /* 每个flex item占据相等的空间 */
margin: 0 10px; /* 图表之间的间距 */
}
</style>
<!-- 引入刚刚下载的 ECharts 文件 -->
<script src="/static/echarts.js"></script>
</head>
<body>
<!-- 为 ECharts 准备一个定义了宽高的 DOM -->
<div class="chart-container">
<div class="chart-row">
<div class="chart-item" id="pie" style="width: 800px;height:400px;"></div>
<div class="chart-item" id="bar" style="width: 800px;height:400px;"></div>
</div>
<div class="chart-row">
<div class="chart-item" id="line" style="width: 800px;height:400px;"></div>
<div class="chart-item" id="barstack" style="width: 800px;height:400px;"></div>
</div>
</div>
<script type="text/javascript">
// 基于准备好的dom,初始化echarts实例
var pieChart = echarts.init(document.getElementById('pie'));
var barChart = echarts.init(document.getElementById('bar'));
var lineChart = echarts.init(document.getElementById('line'));
var barStackChart = echarts.init(document.getElementById('barstack'))
// 使用刚指定的配置项和数据显示图表。
pieChart.setOption({{ pie_options | safe }});
barChart.setOption({{ bar_options | safe }});
lineChart.setOption({{ line_options | safe }});
barStackChart.setOption({{ barstack_options | safe }});
</script>
</body>
</html>
以下面的数据为例,PySpark已经将这些数据存储在Linux系统的MySQL的test数据库,先读取这些数据,然后Pyecharts绘制图表,Flaskk将图表渲染到页面,制作成可视化大屏,可视化大屏的布局为2行2列。
总体国家分析(data_ttl_country):按国家(Country)分组,计算各组的成交量(Quantity)和成交额(Total_Price)。
总体星期几分析(data_ttl_weekday):按星期(DayOfWeek)分组,计算各组的成交量(Quantity)和成交额(Total_Price)。
总体时间分析(按年和月)(data_ttl_time):按年(Year)、月(Month)分组,计算各组的成交量(Quantity)和成交额(Total_Price)。
总体国家时间分析(按国家、年和月)(data_ttl_country_time):按国家(Country)、年(Year)、月(Month)分组,计算各组的成交量(Quantity)和成交额(Total_Price)。
Flask渲染的可视化大屏如下图所示。
PyEcharts绘制的图表更精致美观,图形也更丰富。本案例有很多图形,可以将它们渲染到可视化大屏,如果一个页面放不下,可以渲染到几个页面。
5 总结
(1)季节性销售波动分析:数据明确显示,英国市场在2011年11月出现了显著的销售量激增,这一趋势很可能与年度购物季(如黑色星期五、圣诞节前购物潮)相吻合,随后在12月虽略有回落,但仍维持较高水平,反映出节日促销活动的持续影响。
(2)交易量与单笔交易价值分析:11月的高销售量主要得益于交易次数的增加,表明消费者在该月更加活跃;而12月虽然销售量稍减,但可能由于节日期间高端礼品或大宗商品的购买增加,导致单笔交易金额显著上升,体现了市场消费结构的变化。
(3)季节性产品策略:进一步确认了产品销量受季节性因素影响显著,即便是年度畅销产品如“PAPER CHAIN KIT 50’s CHRISTMAS”也表现出强烈的季节特征。这要求商家需提前规划库存,优化供应链管理,以应对季节性需求波动。
(4)小众产品高销量现象:注意到像“PAPER CRAFT, LITTLE BIRDIE”这样的产品,尽管交易次数极少,但单笔交易的高销量使其位居前列。这提示商家应重视小众市场,可能通过限量发售、定制服务等方式提升产品附加值,吸引特定消费群体。
(5)商品组合与购买习惯:通过关联规则分析,发现消费者倾向于购买同一产品的不同颜色版本,如杯子和碟子的组合购买,这反映了消费者对产品多样性和协调性的需求。商家可据此优化产品组合策略,推出更多配套产品或套餐优惠,提升购买转化率。
下一步工作:
(1)增强市场细分与个性化营销:基于用户购买历史、偏好及行为数据,进行更细致的市场细分,实施个性化营销策略。例如,针对高频购买者提供会员特权或积分奖励,对偏好特定类型产品的消费者推送定制化促销信息。
(2)优化库存管理与预测模型:利用历史销售数据结合季节性因素、市场趋势预测模型,提高库存管理的精确度和灵活性,减少库存积压和缺货风险,同时优化资金占用。
(3)跨品类关联营销:除了同一产品的不同颜色或款式外,探索跨品类的商品组合推荐,如基于用户购买习惯分析,推荐与已购商品相辅相成的其他产品,增加购物篮平均价值。
参考资料:
1.从0开始学习pyspark–Spark DataFrame数据的选取与访问[第5节]:https://blog.csdn.net/weixin_43817712/article/details/140127703
2.大数据–关联规则挖掘案例:https://blog.csdn.net/qq_51641196/article/details/128478588
3.Pyspark+关联规则 Kaggle购物篮分析案例:https://blog.csdn.net/thorn_r/article/details/138351087
4.Spark 关联规则挖掘:https://blog.csdn.net/weixin_39709476/article/details/109223271
5.Pandas vs Spark:获取指定列的N种方式:https://blog.csdn.net/weixin_43841688/article/details/115222979
6.Spark官网:https://spark.apache.org/docs/latest/api/python/reference/index.html