自行车销售案例分析

本文是对自行车销售公司案例分析的一个总结,记录了整个项目需求分析与实现的过程,主要任务是使用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表的字段信息如下:


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')
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为部门数据库引擎
dw_amount_diff表

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。
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的数据更新,然后通过可视化看板分析前天的销售情况。

你可能感兴趣的:(自行车销售案例分析)