本文是对自行车销售公司案例分析的一个总结,记录了整个项目需求分析与实现的过程,主要任务是使用python实现自动化日常业务分析需求和设置指标预警线,并且连接到PowerBI实现可视化,最终将整个分析成果展示出来。
分析成果的链接:销售报表
本文目录
- 项目背景
- 需求分析与实现
- 可视化报表的制作
一、项目背景
Works Cycles是一所虚构的自行车制造公司,该公司生产和销售自行车到全国各地的商业市场。主要销售产品有以下四种:1、自产的自行车;2自产的自行车部件,例如车轮,踏板或制动组件;3、从供应商处购买的自行车服装;4、从供应商处购买的自行车配件;其中自产的自行车作为占销售收入主要地位。
项目数据来源:数据来源于adventure Works公司的的样本数据库。
二、 需求分析与实现
1、项目目标
通过现有数据监控商品的销售情况,并且获取最新的商品销售趋势,以及区域分布情况,为公司的制造和销售提供指导性建议,以增加公司的收益。
2、项目流程
- mysql数据源观察利用,确定日常需要分析指标
- 添加多进程,并调用多进程进行读写数据库
- 利用python进行加工处理,设置预警线,及时发现异常指标
- 在服务器上部署代码,让其每日自动更新
1、 mysql数据源观察利用,确定日常需要分析指标
登录主数据库,在主数据库中ods_sales_orders订单明细表和ods_customer每日新增用户表两张表是用来记录每天的销售的信息表,每天下午6点都会进行更新。
结合ods_sales_orders订单明细表和ods_customer每日新增用户表的信息和项目目标罗列日常需要分析指标如下:
a.分析维度:
- 时间维度——年、季度、月、日
- 地区维度——省份、城市
- 产品维度——产品类别、产品子类
b. 分析指标:
- 总销售额
- 总销量
- 客单价
- 总目标销售额
- 总目标销售量
- 销售额、销量目标达成率
- 同比去年销售额、销量、客单价
- 不同维度(时间、地区、产品)下的销售额、订单量
- 本月至今的销售趋势
2、添加多进程,并使用多进程进行读写数据库
a.创建辅助表dim_date_df
为了每天能自动更新可视化看板,需要创建一个判断时间的辅助表dim_date_df,然后存入部门数据库中,创建dim_date_df的代码文件为create_dim_date.py,部分代码如下:
start_date = '2019-01-01'#由于日常分析只用到去年到目前的数据,所以只从2019-01-01的开始
end_date = datetime.date.today().strftime("%Y-%m-%d")
dim_date = {'create_date':pd.date_range(start=start_date,end=end_date)}
dim_date_df = pd.DataFrame(dim_date)
end_date = pd.to_datetime(end_date)
current_year = end_date.year
last_year = current_year - 1# 上一年
yesterday = end_date + datetime.timedelta(days=-1)# 是否为昨天
is_21_day_before = end_date + datetime.timedelta(days=-21) # 21天前
today = end_date# 是否为今天
current_month = end_date.month # 是否为当前月
current_quarter = end_date.quarter# 是否为当前季度
dim_date_df['year'] = dim_date_df['create_date'].apply(lambda x: x.year)
dim_date_df['month'] = dim_date_df['create_date']. apply(lambda x: x.month)
dim_date_df['day'] = dim_date_df['create_date'].apply(lambda x: x.day)
dim_date_df['quarter'] = dim_date_df['create_date'].apply(lambda x: x.quarter)
#判断是当年/去年/当月/当季/昨天/今天,如果是为1,否则为0
dim_date_df['is_current_year'] = dim_date_df['create_date'].apply(lambda x: 1 if x.year == current_year else 0)
dim_date_df['is_last_year'] = dim_date_df['create_date'].apply(lambda x: 1 if x.year == last_year else 0)# 是否为去年
dim_date_df['is_yesterday'] = dim_date_df['create_date'].apply(lambda x: 1 if x == yesterday else 0)# 是否为昨天
dim_date_df['is_today'] = dim_date_df['create_date'].apply(lambda x: 1 if x == today else 0)# 是否为当日
dim_date_df['is_21_day'] = dim_date_df['create_date'].apply(lambda x: 1 if x >= is_21_day_before else 0) # 是否为最近21天前
dim_date_df['is_current_quarter'] = dim_date_df['create_date'].apply(lambda x: 1 if x.quarter == current_quarter and x.year == current_year else 0) # 是否为当季度
最后dim_date_df表的字段信息如下:
b. 添加多进程
本项目读取的读取的项目达到百万级别,如果普通的读写数据,效率很低,为了提高运行效率,充分调用CPU资源进行多进程读写,添加多进程,并调用多进程进行读写数据。
多进程读取数据的代码文件为select_data_by_multiprocessing.py,部分代码如下:
def select_data_one(self,read,zhu_ods,table_name,page_no,page_size):
engine = Connector(read, zhu_ods).pymysqlEngine()#连接数据库引擎函数read为主数据库IP、端口、账号和密码等信息, zhu_ods表示数据库名称
conn = engine.connect()
start_num = (page_no-1)*page_size
df = pd.read_sql_query("select * from {table_name} limit {start_num}, {page_size}".format( table_name=table_name, start_num=start_num, page_size=page_size), con=conn)
conn.close()
return df
engine = Connector(read, zhu_ods).pymysqlEngine()#连接数据库引擎函数read为主数据库IP、端口、账号和密码等信息, zhu_ods表示数据库名称
conn = engine.connect()
df_num = pd.read_sql_query("select count(1) as page_total_num from {table_name}".format(table_name=table_name),con=conn)['page_total_num'].tolist()[0]
conn.close()
pool = Pool(3)
page_num = (df_num//page_size)+1
df_results = []
for page_no in range(1, page_num+1):
df_ = pool.apply_async(func=self.select_data_one, args=(datafrog_read,adventure_ods,table_name,page_no,page_size))
df_results.append(df_)
pool.close()
pool.join()
end_result = [result.get() for result in df_results]
end_df = pd.concat(end_result, axis=0, sort=False)
存储数据到部门的数据库的代码文件为insert_data_by_multiprocessing.py,部分代码如下
def insert_data_one(self, dataframe, table):
engine =Connector('tosql','bumen_adventure').pymysqlEngine()
##连接数据库引擎函数tosql为部门数据库IP、端口、账号和密码等信息, bumen_adventure表示数据库名称
conn = engine.connect()
dataframe.to_sql(table, con=conn, if_exists="append", index=False)
conn.close()
dataframe.reset_index(drop=True, inplace=True)
list_of_dfs = [dataframe.loc[i:i + 4999, :] for i in range(0, len(dataframe), 5000)]
print(len(list_of_dfs))
pool = Pool(3)
for item in list_of_dfs:
item = df2
pool.apply_async(func=self.insert_data_one, args=(item, table)) #table数据库表名
pool.close()
pool.join()
3、利用python进行加工处理,设置预警线,及时发现异常指标
a. 利用python进行不同维度的加工处理
1、时间维度进行数据加工,设置预警线,及时发现异常指标,对应的代码文件为dw_order_by_day.py,其中部分代码如下:
sum_amount_order = SelectData().select_data_many('read','zhu_ods','ods_sales_orders', 50000)
#调用多进程select_data_by_multiprocessing.py中的函数进行多进程读取订单明细表'ods_sales_orders','read'为主数据库的IP、端口、账号和密码,'zhu_ods'为主数据库名称
sum_amount_order = sum_amount_order.groupby(by='create_date').agg(
{'unit_price': sum, 'customer_key': pd.Series.nunique}).reset_index()
sum_amount_order.rename(columns={'unit_price': 'sum_amount',
'customer_key': 'sum_order'},
inplace=True)
sum_amount_order['amount_div_order'] = \
sum_amount_order['sum_amount'] / sum_amount_order['sum_order']
#按日期分组求销量、销售额、客单价
销售目标的设定(目标设定这里根据随机数,实际工作中要跟实际业务来定)
sum_amount_goal_list = []
sum_order_goal_list = []
create_date_list = list(sum_amount_order['create_date']) # 获取sum_amount_order中的create_date
for i in create_date_list:
a = random.uniform(0.85, 1.1) # 生成一个在[0.85,1.1]随机数
b = random.uniform(0.85, 1.1)
amount_goal = list(sum_amount_order[sum_amount_order['create_date'] == i]
['sum_amount'])[0] * a # 对应日期下生成总金额(sum_amount)*a的列
order_goal = list(sum_amount_order[sum_amount_order['create_date'] == i]
['sum_order'])[0] * b # 对应日期下生成总订单数(sum_order)*a的列
sum_amount_goal_list.append(amount_goal) # 将生成的目标值加入空列表
sum_order_goal_list.append(order_goal)
sum_amount_order_goal = pd.concat([sum_amount_order, pd.DataFrame(
{'sum_amount_goal': sum_amount_goal_list, 'sum_order_goal':
sum_order_goal_list})], axis=1)
- 从部门数据库中读取dim_date_df日期维度表,由于这里的数据较小,则不必要调用多进程读取
date_sql = """
select create_date,
is_current_year,
is_last_year,
is_yesterday,
is_today,
is_current_month,
is_current_quarter
from dim_date_df"""
date_info = pd.read_sql_query(date_sql, con=adventure_conn_tosql)
- 通过主键“create_date”将date_info和sum_amount_order_goal联结起来
sum_amount_order_goal['create_date'] = sum_amount_order_goal['create_date']. \
apply(lambda x: x.strftime('%Y-%m-%d')) # 转化create_date格式为标准日期格式
amount_order_by_day = pd.merge(date_info,sum_amount_order_goal,
on='create_date', how='left')
- 设置当天销售额和销量预警值,自动化发邮件通知(这里销售额和销量预警值都是设置为目标的50%,
if sum_amount_order_goal[sum_amount_order_goal['is_today'] ==1]['sum_order']<=sum_amount_order_goal[sum_amount_order_goal['is_today'] ==1]['sum_order_goal']*0.5 or sum_amount_order_goal[sum_amount_order_goal['is_today'] ==1]['sum_amount']<=sum_amount_order_goal[sum_amount_order_goal['is_today'] ==1]['sum_amount_goal']*0.5:
receivers=['[email protected]']
message = MIMEMultipart()
content_text='需要注意销售指标低于目标值的50%'
message.attach(MIMEText(content_text,'plain','utf-8'))
message['Subject'] = '销售指标低于预警值'
message['From'] = 'reporter<%s>'%([email protected])
reces = ''
for i in range(len(receivers)):
reces = reces+',receiveer%d<%s>'%(i+1,receivers[i])
message['To'] = reces
smtpObj = smtplib.SMTP_SSL(host='smtp.163.com')
smtpObj.connect('smtp.163.com', '465')
smtpObj.login('[email protected]', ******)
smtpObj.sendmail('[email protected]',receivers,
message.as_string())
smtpObj.quit()
- 调用多进程,将amount_order_by_day存进部门数据库,表名dw_order_by_day
InsertData().insert_data_many(amount_order_by_day, table='dw_order_by_day')
2、求不同时间维度的销售额、销量、客单价同比表,代码文件为dw_order_diff.py,部分代码如下:
dw_order_by_day = pd.read_sql_query("select * from dw_order_by_day_{}",con=adventure_conn_tosql)
#con=adventure_conn_tosql为部门数据库的引擎。
dw_order_by_day['create_date'] = pd.to_datetime(dw_order_by_day['create_date'])
def diff(stage, indictor):
try:
current_stage_indictor = dw_order_by_day[dw_order_by_day[stage] == 1][indictor].sum()
before_stage_list = list(
dw_order_by_day[dw_order_by_day[stage] == 1]['create_date'] + datetime.timedelta(days=-365))
before_stage_indictor = dw_order_by_day[dw_order_by_day['create_date']. \
isin(before_stage_list)][indictor].sum()
return current_stage_indictor, before_stage_indictor
except Exception as e:
logger.info("diff异常,报错信息:{}".format(e))
if __name__ == "__main__":
"""各阶段的金额"""
today_amount, before_year_today_amount = diff('is_today', 'sum_amount')
yesterday_amount, before_year_yesterday_amount = diff('is_yesterday', 'sum_amount')
month_amount, before_year_month_amount = diff('is_current_month', 'sum_amount')
quarter_amount, before_year_quarter_amount = diff('is_current_quarter', 'sum_amount')
year_amount, before_year_year_amount = diff('is_current_year', 'sum_amount')
"""各阶段的订单数"""
today_order, before_year_today_order = diff('is_today', 'sum_order')
yesterday_order, before_year_yesterday_order = diff('is_yesterday', 'sum_order')
month_order, before_year_month_order = diff('is_current_month', 'sum_order')
quarter_order, before_year_quarter_order = diff('is_current_quarter', 'sum_order')
year_order, before_year_year_order = diff('is_current_year', 'sum_order')
amount_dic = {'today_diff': [today_amount / before_year_today_amount - 1,
today_order / before_year_today_order - 1,
(today_amount / today_order) / (before_year_today_amount /
before_year_today_order) - 1],
'yesterday_diff': [yesterday_amount / before_year_yesterday_amount - 1,
yesterday_order / before_year_yesterday_order - 1,
(yesterday_amount / yesterday_order) / (before_year_yesterday_amount /
before_year_yesterday_order) - 1],
'month_diff': [month_amount / before_year_month_amount - 1,
month_amount / before_year_month_amount - 1,
(month_amount / month_order) / (before_year_month_amount /
before_year_month_order) - 1],
'quarter_diff': [quarter_amount / before_year_quarter_amount - 1,
quarter_amount / before_year_quarter_amount - 1,
(quarter_amount / quarter_order) / (before_year_quarter_amount /
before_year_quarter_order) - 1],
'year_diff': [year_amount / before_year_year_amount - 1,
year_amount / before_year_year_amount - 1,
(year_amount / year_order) / (before_year_year_amount /
before_year_year_order) - 1],
'flag': ['amount', 'order', 'avg']} # 做符号简称,横向提取数据方便
- 将amount_dic表写入部门数据库,表名为dw_amount_diff
amount_diff.to_sql('dw_amount_diff', con=adventure_conn_tosql,
if_exists='append', index=False)
#adventure_conn_tosql为部门数据库引擎
3、求地区维度(省份、城市)和 产品维(产品类别、产品子类),代码文件为update_sum_data_multiprocessing.py,部分代码如下:
order_info= SelectData().select_data_many('read', 'adventure_ods', 'ods_sales_orders', 50000)
#调用多进程读取订单明细表ods_sales_orders,'read':主数据库IP、端口、账号和密码,'adventure_ods':数据库名称
order_info=order_info[['sales_order_key','create_date','customer_key','english_product_name','cpzl_zw','cplb_zw','unit_price']]
#获取订单明细表中需要分析的字段信息变成order_info
date_sql = """
select create_date,
is_current_year,
is_last_year,
is_yesterday,
is_today,
is_current_month,
is_current_quarter
from dim_date_df"""
date_info = pd.read_sql_query(date_sql, con=adventure_conn_tosql)
#获取日期/当年/去年/当月/当季/当天/昨天的字段的日期维度表date_info,adventure_conn_tosq为主数据库引擎
customer_info=SelectData().select_data_many('read','adventure_ods','ods_customer', 50000)
customer_info=customer_info[['customer_key','chinese_territory','chinese_province','chinese_city']]
#获取客户的区域分布的字段表customer_info
sales_customer_order = pd.merge(order_info, customer_info, left_on='customer_key',
right_on='customer_key', how='left')
sales_customer_order=sales_customer_order[['sales_order_key','create_date','customer_key','english_product_name','cpzl_zw','cplb_zw','unit_price',
'chinese_territory',
'chinese_province',
'chinese_city'
]]
#将order_info和customer_info表通过customer_key联结起来,提取订单主键/订单日期/客户编号/产品名/产品子类/产品类别/产品单价/所在区域/所在省份/所在城市
sum_customer_order = sales_customer_order.groupby(["create_date", "english_product_name", "cpzl_zw", "cplb_zw",
"chinese_territory", "chinese_province",
"chinese_city"], as_index=False). \
agg({'sales_order_key': pd.Series.nunique, 'customer_key': pd.Series.nunique,
"unit_price": "sum"}).rename(columns={'sales_order_key': 'order_num', \
'customer_key': 'customer_num', 'unit_price': 'sum_amount', \
"english_product_name": "product_name"})
#按照 订单日期/产品名/产品子类/产品类别/所在区域/所在省份/所在城市的逐级聚合表,获得订单总量/客户总量/销售总金额
sum_customer_order['create_date']=sum_customer_order['create_date'].apply(lambda x:x.strftime('%Y-%m-%d')) sum_customer_order=pd.merge(sales_customer_order,date_info,on='create_date',how='inner')
#转化订单日期为字符型格式和与日期维度表date_info融合
InsertData().insert_data_many(sum_customer_order, table='dw_customer_order')
#调用多进程将sum_customer_order存储到部门数据库,在部门数据库形成表名dw_customer_order。
4、在服务器上部署代码,让其每日自动更新
主数据库基本在6点前的时候会更新所有数据,为了让聚合表每天在主数据库后更新一遍获取自动更新数据,将前面4个聚合表的代码文件:create_dim_date.py、dw_order_by_day _multiprocessing.py、dw_order_diff.py、update_sum_data_multiprocessing.py设定在一张代码文件schedule_job_test.py中按顺序进行定时任务操作,然后再将schedule_job_test.py挂在linux系统后台就行运行,chedule_job_test.py部分代码如下
def job1():
"""
dw_dim_df 时间维度表
"""
print('Job1:每天5:45执行一次')
print('Job1-startTime:%s' % (datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')))
os.system(
"/home/anaconda3/bin/python3 /home/create_dim_date.py >> /home/llx_logs/create_dim_date_schedule.log 2>&1 &")
time.sleep(20)
print('Job1-endTime:%s' % (datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')))
print('------------------------------------------------------------------------')
def job2():
"""
dw_order_by_day 每日环比表
"""
print('Job2:每天6:00执行一次')
print('Job2-startTime:%s' % (datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')))
os.system(
"/home/anaconda3/bin/python3/home/dw_order_by_day _multiprocessing.py >> /home/llx_logs/dw_order_by_day_schedule.log 2>&1 &")
time.sleep(20)
print('Job1-endTime:%s' % (datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')))
print('------------------------------------------------------------------------')
def job3():
"""
dw_order_diff 同比数据表
"""
print('Job3:每天6:20执行一次')
print('Job3-startTime:%s' % (datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')))
os.system(
"/home/anaconda3/bin/python3 /home/dw_order_diff.py >> /home/llx_logs/dw_order_diff_schedule.log 2>&1 &")
time.sleep(20)
print('Job3-endTime:%s' % (datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')))
print('------------------------------------------------------------------------')
def job4():
print('Job4:每天6:40执行一次')
print('Job4-startTime:%s' % (datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')))
os.system(
"/home/anaconda3/bin/python3 /home/update_sum_data_multiprocessing.py >> /home/llx_logs/update_sum_data_schedule.log 2>&1 &")
time.sleep(20)
print('Job4-endTime:%s' % (datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')))
print('------------------------------------------------------------------------')
if __name__ == '__main__':
schedule.every().day.at('05:45').do(job1)
schedule.every().day.at('06:00').do(job2)
schedule.every().day.at('06:20').do(job3)
schedule.every().day.at('06:40').do(job4)
while True:
schedule.run_pending()
time.sleep(10)
print("wait", datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'))
- schedule_job_test.py挂在Linux系统后面的代码如下:
"/home/anaconda3/bin/python3 /home/schedule_job_test.py >> /home/llx_logs/schedule_job_test_schedule.log 2>&1 &")
三、 可视化报表的制作
选择合适的可视化工具,从多个维度展示销售情况。
1、power bi 连接部门数据库,实现bi数据的自动更新。
2、核心操作
- 可视化工具:这里用到的可视化工具有折线图、柱形图、折线-柱形组合图、KPI、卡片、切片器、地图等。可以根据需要选择图例、轴、列,以及设置数据处理方式,求和、平均值、最大值、最小值等。
- 筛选器:有三种筛选器:视觉对象、此页、所有页面。这里用于日期、区域等字段的筛选。特别注意的是:日期筛选的时候,用的是判断日期字段,如:is_today、is_yestday、is_year等字段
- 书签窗格:这里将按钮和书签结合使用,用于制作导航栏和动态图表。
- 选择窗格:可以选择显示/隐藏视觉对象,这里用于bike和非bike类商品图表的切换显示。
3、报表展示
报表一共有3大块,包括主页、时间趋势图、区域分布图。
a. 主页展示内容:
- 基本销售指标,包括销售额、订单量、客单价、销售KPI情况等
- 从时间维度分析年度、季度、月度、昨天、日销售情况
- 从地区维度分析在各大区域的销售情况
-从地区维度分析在各大区域的热销自行车产品的销售情况 -
从产品类别维度分析各类商品的销售占比,以及自行车类与非自行车类的子类的占比
b. 时间趋势图展示内容:
- 展示本月内销售额、订单量、客单价、销售目标完成情况等指标销售趋势对比分析
-
展示本月内自行车和非自行车的销量趋势分析(非自行车就分析销量)
当然,这里还可以增加更多的时间维度,比如年、周、过去30天等。
c. 区域分布图展示内容:
- 按照省、城市,逐级展示销售额和订单量等指标,以及自行车类和非自行车类区域分布
-
区域、城市类型切片器
4、power bi的数据更新
进行每天的power bi的数据更新,然后通过可视化看板分析前天的销售情况。