数据分析的基本过程一般分为以下几个部分:提出问题、理解数据、数据清洗、构建模型、数据可视化
本项目带你根据以上过程详细分析朝阳医院药品销售数据!
1.提出问题
在数据分析之前,我们先要明确分析目标是什么,这样可以避免我们像无头苍蝇一样拿着数据无从下手,也可以帮助我们更高效的选取数据,进行分析研究。
本次的分析目标是从销售数据中分析出以下业务指标:
1)月均消费次数
2)月均消费金额
3)客单价
4)消费趋势
有了分析目标,我们再来关注一下数据情况。
2.理解数据
1)导入数据包,提取数据文件
In [1]:
#导入numpy、pandas包import numpy as npimport pandas as pd#导入数据salesDf = pd.read_excel('/home/kesci/input/medical9242/朝阳医院2018年销售数据.xlsx')
2)查看导入数据的基本状况
In [2]:
#查看导入数据的类型type(salesDf)
Out[2]:
pandas.core.frame.DataFrame
In [3]:
salesDf.dtypes
Out[3]:
购药时间 object
社保卡号 float64
商品编码 float64
商品名称 object
销售数量 float64
应收金额 float64
实收金额 float64
dtype: object
In [4]:
salesDf.shape
Out[4]:
(6578, 7)
In [5]:
#查看列名salesDf.columns
Out[5]:
Index(['购药时间', '社保卡号', '商品编码', '商品名称', '销售数量', '应收金额', '实收金额'], dtype='object')
In [6]:
#查看每列数据的统计数目salesDf.count()
Out[6]:
购药时间 6576
社保卡号 6576
商品编码 6577
商品名称 6577
销售数量 6577
应收金额 6577
实收金额 6577
dtype: int64
In [7]:
#查看前五列salesDf.head()
Out[7]:
购药时间 | 社保卡号 | 商品编码 | 商品名称 | 销售数量 | 应收金额 | 实收金额 | |
---|---|---|---|---|---|---|---|
0 | 2018-01-01 星期五 | 1.616528e+06 | 236701.0 | 强力VC银翘片 | 6.0 | 82.8 | 69.00 |
1 | 2018-01-02 星期六 | 1.616528e+06 | 236701.0 | 清热解毒口服液 | 1.0 | 28.0 | 24.64 |
2 | 2018-01-06 星期三 | 1.260283e+07 | 236701.0 | 感康 | 2.0 | 16.8 | 15.00 |
3 | 2018-01-11 星期一 | 1.007034e+10 | 236701.0 | 三九感冒灵 | 1.0 | 28.0 | 28.00 |
4 | 2018-01-15 星期五 | 1.015543e+08 | 236701.0 | 三九感冒灵 | 8.0 | 224.0 | 208.00 |
3.数据清洗
取得了数据,并不能马上就开始进行数据分析。我们得到的数据通常并不是完全符合我们分析要求的,而且可能存在缺失值、异常值,这些数据都会使我们的分析结果产生偏差。所以在分析之前,需要进行子集选择、缺失数据补充、异常值处理、数据类型转换等多个步骤。这些都属于数据清理的范畴。在数据分析中,通常有多达60%的时间是花在数据清洗中的。通常的清洗步骤有以下几步:• 选择子集
• 列名重命名
• 缺失数据处理
• 数据类型转换
• 数据排序
• 异常值处理
这些步骤有些不是一步就能完成的,可能需要重复操作。
现在开始对药店销售数据进行数据清洗。
1)选择子集
药店销售数据中,项目较少,选择子集可以忽略,我们从列名重命名开始。
2)列名重命名
销售数据集,购药时间显示为销售时间更为合理,我们先把这个项目名称做一下变更。
In [8]:
#购药时间->销售时间nameChangeDict = {'购药时间':'销售时间'}#参数inplace=True表示覆盖元数据集salesDf.rename(columns = nameChangeDict,inplace=True)
3)缺失数据处理
对于缺失数据,我们可以有几种处理方法:
▪ 删除
当缺失数据占总数据量的比例很小的时候,我们通常采用删除的处理方法。
▪ 合理值填充
在某些不适合删除的场合,我们有时候也会对缺失数据进行合理值填充,如平均值,中位数,相邻数据等等。
In [9]:
#首先查看一下哪些项目存在缺失值salesDf.isnull().any()
Out[9]:
销售时间 True
社保卡号 True
商品编码 True
商品名称 True
销售数量 True
应收金额 True
实收金额 True
dtype: bool
好吧,每个项目都存在缺失值。在这个销售数据中,销售时间和社保卡号是必须项目,不可或缺。所以我们在这里只把销售时间和社保卡号有缺失的数据做删除处理。我们来查看一下销售时间和社保卡缺失的数据大小,然后做删除处理。
In [10]:
#查看一下缺失值的数量#通常可以用isnull函数来查找缺失值salesDf[salesDf[['销售时间','社保卡号']].isnull().values == True]
Out[10]:
销售时间 | 社保卡号 | 商品编码 | 商品名称 | 销售数量 | 应收金额 | 实收金额 | |
---|---|---|---|---|---|---|---|
6570 | NaN | 11778628.0 | 2367011.0 | 高特灵 | 10.0 | 56.0 | 56.00 |
6571 | 2018-04-25 星期二 | NaN | 2367011.0 | 高特灵 | 2.0 | 11.2 | 9.86 |
6574 | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
6574 | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
In [11]:
#序号6574因为销售时间和社保卡号都缺失,所以会出现两次。所以我们要去掉一下重复数据。naDf = salesDf[salesDf[['销售时间','社保卡号']].isnull().values == True].drop_duplicates()naDf
Out[11]:
销售时间 | 社保卡号 | 商品编码 | 商品名称 | 销售数量 | 应收金额 | 实收金额 | |
---|---|---|---|---|---|---|---|
6570 | NaN | 11778628.0 | 2367011.0 | 高特灵 | 10.0 | 56.0 | 56.00 |
6571 | 2018-04-25 星期二 | NaN | 2367011.0 | 高特灵 | 2.0 | 11.2 | 9.86 |
6574 | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
从上面可以清楚看出销售时间和社保卡号缺失的数据一共有三条,当数据量大的时候我们可以只显示条数,不显示数据内容
In [12]:
#缺失数据行数naDf.shape[0]
Out[12]:
3
现在把这些缺失数据进行删除
In [13]:
#含有销售时间和社保卡号的缺失数据删除salesDf = salesDf.dropna(subset=['销售时间','社保卡号'],how = 'any')#删除后数据集规模显示salesDf.shape
Out[13]:
(6575, 7)
在数据删除后要及时更新一下最新的序号,不然可能会产生问题。
In [14]:
#重命名行名(index):排序后的列索引值是之前的行号,需要修改成从0到N按顺序的索引值salesDf=salesDf.reset_index(drop=True)
4)数据类型转换
▪ 数量、金额项目:从字符串类型转换为数值(浮点型)类型
In [15]:
salesDf['销售数量'] = salesDf['销售数量'].astype('float')salesDf['应收金额'] = salesDf['应收金额'].astype('float')salesDf['实收金额'] = salesDf['实收金额'].astype('float')print('转换后的数据类型:\n',salesDf.dtypes)
转换后的数据类型:
销售时间 object
社保卡号 float64
商品编码 float64
商品名称 object
销售数量 float64
应收金额 float64
实收金额 float64
dtype: object
▪ 日期项目:从字符串类型转换为日期类型 销售日期中包含了日期和星期,我们只要保留日期内容即可。这里用一个自定义的函数dateChange来实现这个功能。
In [16]:
#日期转换def dateChange(dateSer):
dateList = []
for i in dateSer:
#例如2018-01-01 星期五,分割后为:2018-01-01
str = i.split(' ')[0]
dateList.append(str)
dateChangeSer = pd.Series(dateList)
return dateChangeSerdateChangeSer = dateChange(salesDf['销售时间'])dateChangeSer
Out[16]:
0 2018-01-01
1 2018-01-02
2 2018-01-06
3 2018-01-11
4 2018-01-15
5 2018-01-20
6 2018-01-31
7 2018-02-17
8 2018-02-22
9 2018-02-24
10 2018-03-05
11 2018-03-05
12 2018-03-05
13 2018-03-07
14 2018-03-09
15 2018-03-15
16 2018-03-15
17 2018-03-15
18 2018-03-20
19 2018-03-22
20 2018-03-23
21 2018-03-24
22 2018-03-24
23 2018-03-28
24 2018-03-29
25 2018-04-05
26 2018-04-07
27 2018-04-13
28 2018-04-22
29 2018-05-01
...
6545 2018-04-05
6546 2018-04-05
6547 2018-04-09
6548 2018-04-10
6549 2018-04-10
6550 2018-04-10
6551 2018-04-12
6552 2018-04-13
6553 2018-04-13
6554 2018-04-14
6555 2018-04-15
6556 2018-04-15
6557 2018-04-15
6558 2018-04-15
6559 2018-04-16
6560 2018-04-17
6561 2018-04-18
6562 2018-04-21
6563 2018-04-22
6564 2018-04-24
6565 2018-04-25
6566 2018-04-25
6567 2018-04-25
6568 2018-04-26
6569 2018-04-26
6570 2018-04-27
6571 2018-04-27
6572 2018-04-27
6573 2018-04-27
6574 2018-04-28
Length: 6575, dtype: object
In [17]:
salesDf['销售时间'] = dateChangeSersalesDf.head()
Out[17]:
销售时间 | 社保卡号 | 商品编码 | 商品名称 | 销售数量 | 应收金额 | 实收金额 | |
---|---|---|---|---|---|---|---|
0 | 2018-01-01 | 1.616528e+06 | 236701.0 | 强力VC银翘片 | 6.0 | 82.8 | 69.00 |
1 | 2018-01-02 | 1.616528e+06 | 236701.0 | 清热解毒口服液 | 1.0 | 28.0 | 24.64 |
2 | 2018-01-06 | 1.260283e+07 | 236701.0 | 感康 | 2.0 | 16.8 | 15.00 |
3 | 2018-01-11 | 1.007034e+10 | 236701.0 | 三九感冒灵 | 1.0 | 28.0 | 28.00 |
4 | 2018-01-15 | 1.015543e+08 | 236701.0 | 三九感冒灵 | 8.0 | 224.0 | 208.00 |
在做完转化后再观察一下有没有产生新的缺失值
In [18]:
salesDf['销售时间'].isnull().any()
Out[18]:
False
In [19]:
salesDf.dtypes
Out[19]:
销售时间 object
社保卡号 float64
商品编码 float64
商品名称 object
销售数量 float64
应收金额 float64
实收金额 float64
dtype: object
数据没有产生新的缺失,我们继续向下,把销售时间的数据类型转为日期型。
In [20]:
dateSer=pd.to_datetime(salesDf['销售时间'], format = '%Y-%m-%d', errors='coerce')dateSer
Out[20]:
0 2018-01-01
1 2018-01-02
2 2018-01-06
3 2018-01-11
4 2018-01-15
5 2018-01-20
6 2018-01-31
7 2018-02-17
8 2018-02-22
9 2018-02-24
10 2018-03-05
11 2018-03-05
12 2018-03-05
13 2018-03-07
14 2018-03-09
15 2018-03-15
16 2018-03-15
17 2018-03-15
18 2018-03-20
19 2018-03-22
20 2018-03-23
21 2018-03-24
22 2018-03-24
23 2018-03-28
24 2018-03-29
25 2018-04-05
26 2018-04-07
27 2018-04-13
28 2018-04-22
29 2018-05-01
...
6545 2018-04-05
6546 2018-04-05
6547 2018-04-09
6548 2018-04-10
6549 2018-04-10
6550 2018-04-10
6551 2018-04-12
6552 2018-04-13
6553 2018-04-13
6554 2018-04-14
6555 2018-04-15
6556 2018-04-15
6557 2018-04-15
6558 2018-04-15
6559 2018-04-16
6560 2018-04-17
6561 2018-04-18
6562 2018-04-21
6563 2018-04-22
6564 2018-04-24
6565 2018-04-25
6566 2018-04-25
6567 2018-04-25
6568 2018-04-26
6569 2018-04-26
6570 2018-04-27
6571 2018-04-27
6572 2018-04-27
6573 2018-04-27
6574 2018-04-28
Name: 销售时间, Length: 6575, dtype: datetime64[ns]
In [21]:
dateSer.isnull().any()
Out[21]:
True
In [22]:
compareDf = pd.DataFrame(dateSer[dateSer.isnull()],salesDf[dateSer.isnull()]['销售时间'])compareDf
Out[22]:
销售时间 | |
---|---|
销售时间 | |
2018-02-29 | NaT |
2018-02-29 | NaT |
2018-02-29 | NaT |
2018-02-29 | NaT |
2018-02-29 | NaT |
2018-02-29 | NaT |
2018-02-29 | NaT |
2018-02-29 | NaT |
2018-02-29 | NaT |
2018-02-29 | NaT |
2018-02-29 | NaT |
2018-02-29 | NaT |
2018-02-29 | NaT |
2018-02-29 | NaT |
2018-02-29 | NaT |
2018-02-29 | NaT |
2018-02-29 | NaT |
2018-02-29 | NaT |
2018-02-29 | NaT |
2018-02-29 | NaT |
2018-02-29 | NaT |
2018-02-29 | NaT |
2018-02-29 | NaT |
查看了下数据,产生空值的原因是因为数据中出现了'2018-02-29'这样实际不存在的日期。在实际应用中,最好能向业务部门询问一下产生的原因,看下是不是因为日期推算不正确导致了这样原因的产生,需不需要将这样的数据进行一下必要的修正。这里就简单的把数据进行删除。
In [23]:
salesDf['销售时间'] = dateSersalesDf.dtypes
Out[23]:
销售时间 datetime64[ns]
社保卡号 float64
商品编码 float64
商品名称 object
销售数量 float64
应收金额 float64
实收金额 float64
dtype: object
In [24]:
salesDf=salesDf.dropna(subset=['销售时间','社保卡号'],how='any')salesDf.shape
Out[24]:
(6552, 7)
In [25]:
salesDf=salesDf.reset_index(drop=True)
5)数据排序 销售记录一般是以销售时间为顺序排列的,所以我们对数据进行一下排序
In [26]:
#按销售时间排序salesDf = salesDf.sort_values(by='销售时间')#再次更新一下序号salesDf = salesDf.reset_index(drop = True)
6)异常值处理
在下面数据集的描述指标中可以看出,存在销售数量为负的数据,这明显是不合理的,我们把这部分数据也进行删除
In [27]:
salesDf.describe()
Out[27]:
社保卡号 | 商品编码 | 销售数量 | 应收金额 | 实收金额 | |
---|---|---|---|---|---|
count | 6.552000e+03 | 6.552000e+03 | 6552.000000 | 6552.00000 | 6552.000000 |
mean | 6.095150e+09 | 1.015031e+06 | 2.384158 | 50.43025 | 46.266972 |
std | 4.888430e+09 | 5.119572e+05 | 2.374754 | 87.68075 | 81.043956 |
min | 1.616528e+06 | 2.367010e+05 | -10.000000 | -374.00000 | -374.000000 |
25% | 1.014290e+08 | 8.614560e+05 | 1.000000 | 14.00000 | 12.320000 |
50% | 1.001650e+10 | 8.615070e+05 | 2.000000 | 28.00000 | 26.500000 |
75% | 1.004898e+10 | 8.687840e+05 | 2.000000 | 59.60000 | 53.000000 |
max | 1.283612e+10 | 2.367012e+06 | 50.000000 | 2950.00000 | 2650.000000 |
In [28]:
#删除异常值:通过条件判断筛选出数据#查询条件querySer=salesDf.loc[:,'销售数量']>0#应用查询条件print('删除异常值前:',salesDf.shape)salesDf=salesDf.loc[querySer,:]print('删除异常值后:',salesDf.shape)
删除异常值前:(6552, 7)
删除异常值后:(6509, 7)
数据清洗完了之后,我们终于可以来搭建我们的模型啦。当然如果在模型搭建过程中再次发现数据异常情况,我们还是要对数据进行进一步的清洗。
4.构建模型
1)业务指标1:月均消费次数=总消费次数 / 月份数
总消费次数:同一天内,同一个人发生的所有消费算作一次消费。这里我们根据列名(销售时间,社区卡号)结合,如果这两个列值同时相同,只保留1条,将重复的数据删除
月份数:数据已经按照销售时间进行排序,只需将最后的数据与第一条数据相减就可换算出月份数
In [29]:
#总消费次数计算kpDf = salesDf.drop_duplicates(subset=['销售时间','社保卡号'])total = kpDf.shape[0]print('总消费次数为:',total)
总消费次数为:5345
In [30]:
#月份数计算startDay = salesDf.loc[0,'销售时间']print('开始日期:',startDay)endDay = salesDf.loc[salesDf.shape[0]-1,'销售时间']print('结束日期:',endDay)monthCount = (endDay - startDay).days//30print('月份数:',monthCount)
开始日期: 2018-01-01 00:00:00
结束日期: 2018-07-18 00:00:00
月份数: 6
In [31]:
#业务指标1:月均消费次数=总消费次数 / 月份数kpi1 = total / monthCountprint('业务指标1:月均消费次数=',kpi1)
业务指标1:月均消费次数= 890.8333333333334
2)指标2:月均消费金额 = 总消费金额 / 月份数
In [32]:
totalMoney = salesDf['实收金额'].sum()kpi2 = totalMoney / monthCountprint('业务指标2:月平均消费金额=',kpi2)
业务指标2:月平均消费金额= 50672.494999999995
3)指标3:客单价=总消费金额 / 总消费次数
In [33]:
kpi3 = kpi2 / kpi1print('业务指标3:客单价=',kpi3)
业务指标3:客单价= 56.88212722170252
4)指标4:消费趋势,画图:折线图
In [34]:
#在进行操作之前,先把数据复制到另一个数据框中,防止对之前清洗后的数据框造成影响groupDf=salesDf#第1步:重命名行名(index)为销售时间所在列的值groupDf.index=groupDf['销售时间']groupDf.head()
Out[34]:
销售时间 | 社保卡号 | 商品编码 | 商品名称 | 销售数量 | 应收金额 | 实收金额 | |
---|---|---|---|---|---|---|---|
销售时间 | |||||||
2018-01-01 | 2018-01-01 | 1.616528e+06 | 236701.0 | 强力VC银翘片 | 6.0 | 82.8 | 69.0 |
2018-01-01 | 2018-01-01 | 1.078916e+08 | 861456.0 | 酒石酸美托洛尔片(倍他乐克) | 2.0 | 14.0 | 12.6 |
2018-01-01 | 2018-01-01 | 1.616528e+06 | 861417.0 | 雷米普利片(瑞素坦) | 1.0 | 28.5 | 28.5 |
2018-01-01 | 2018-01-01 | 1.007397e+10 | 866634.0 | 硝苯地平控释片(欣然) | 6.0 | 111.0 | 92.5 |
2018-01-01 | 2018-01-01 | 1.001429e+10 | 866851.0 | 缬沙坦分散片(易达乐) | 1.0 | 26.0 | 23.0 |
In [35]:
#第2步:分组gb=groupDf.groupby(groupDf.index.month)#第3步:应用函数,计算每个月的消费总额mounthDf=gb.sum()mounthDf
Out[35]:
社保卡号 | 商品编码 | 销售数量 | 应收金额 | 实收金额 | |
---|---|---|---|---|---|
销售时间 | |||||
1 | 6.257155e+12 | 1.073329e+09 | 2527.0 | 53561.6 | 49461.19 |
2 | 4.702493e+12 | 7.438598e+08 | 1858.0 | 42028.8 | 38790.38 |
3 | 6.124761e+12 | 1.007946e+09 | 2225.0 | 45318.0 | 41597.51 |
4 | 7.620230e+12 | 1.226705e+09 | 3010.0 | 54324.3 | 48812.70 |
5 | 5.898556e+12 | 1.004573e+09 | 2225.0 | 51263.4 | 46925.27 |
6 | 5.421001e+12 | 9.289637e+08 | 2328.0 | 52300.8 | 48327.70 |
7 | 3.608900e+12 | 6.259256e+08 | 1483.0 | 32568.0 | 30120.22 |
In [36]:
import matplotlib.pyplot as pltimport seaborn as snsimport matplotlib as mplmpl.rcParams['font.sans-serif'] = ['SimHei']mpl.rcParams['font.serif'] = ['SimHei']sns.set_style("darkgrid",{"font.sans-serif":['simhei', 'Arial']})import matplotlib.pyplot as plt%matplotlib inline#绘制销售数量图plt.plot(mounthDf['销售数量'],color = 'b')
Out[36]:
[<matplotlib.lines.Line2D at 0x7fa6e872e550>]
findfont: Font family ['sans-serif'] not found. Falling back to DejaVu Sans.
四月份为最高点,二月份为前期一个最低点,而且在四月份以后销售一直处于向下的趋势,在记录的日期中,七月份达到了历史最低水平。