朝陽(yáng)醫(yī)院2018年銷售數(shù)據(jù)分析
一、提出問(wèn)題
一般來(lái)說(shuō),數(shù)據(jù)分析師拿到這個(gè)需求之后不能馬上就開始導(dǎo)入然后分析,必須在分析之前要明確自己的分析目的。數(shù)據(jù)分析只是手段,只是工具而已,能不能解決所要分析的問(wèn)題,能不能從數(shù)據(jù)中發(fā)現(xiàn)問(wèn)題,發(fā)現(xiàn)商機(jī),才是重點(diǎn)。
重要的話重復(fù)三遍:
分析之前一定要明確自己這次分析是為了什么
分析之前一定要明確自己這次分析是為了什么
分析之前一定要明確自己這次分析是為了什么
現(xiàn)在有一份朝陽(yáng)醫(yī)院2018年的銷售數(shù)據(jù),不能隨便分析,分析到什么結(jié)果算什么結(jié)果。分析之前我們要有針對(duì)性的分析目標(biāo)。
那么此次分析的目標(biāo)是:
- 月均消費(fèi)次數(shù)
- 月均消費(fèi)金額
- 客單價(jià)
- 消費(fèi)趨勢(shì)
當(dāng)我們清楚的了解自己的分析目標(biāo)之后,就可以開始分析了。
二、理解數(shù)據(jù)
理解數(shù)據(jù)是為了清洗數(shù)據(jù)做準(zhǔn)備的,如果你都不了解你手里的數(shù)據(jù)有多少,分別是什么類型,有多少缺失等,那就無(wú)法進(jìn)行清洗數(shù)據(jù)的工作,從而無(wú)法開始真正的數(shù)據(jù)分析。
# 首先加載數(shù)據(jù)分析常用庫(kù)
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
% matplotlib inline
#然后導(dǎo)入數(shù)據(jù)
sales = pd.read_excel('D:/辦公/數(shù)據(jù)分析/chaoyangyiyuan9062/朝陽(yáng)醫(yī)院2018年銷售數(shù)據(jù).xlsx')
#先看一下總體數(shù)據(jù)的描述統(tǒng)計(jì)分析
sales.describe()

這里我們可以發(fā)現(xiàn)銷售數(shù)量、應(yīng)收金額、實(shí)收金額出現(xiàn)負(fù)值,這些肯定是屬于需要清洗篩選的數(shù)據(jù)了。
#再看一下數(shù)據(jù)的真實(shí)情況
sales.head()

#再看一下數(shù)據(jù)信息
sales.info()

通過(guò)以上的理解數(shù)據(jù),我們可以著手開始清洗數(shù)據(jù)了。
三、清洗數(shù)據(jù)
1、選擇子集
在我們獲取到的數(shù)據(jù)中,數(shù)據(jù)量十分龐大,但是不是每一列都是我們所需要分析的呢,不一定,那么這個(gè)時(shí)候就要選擇整個(gè)數(shù)據(jù)中合適的子集去進(jìn)行分析,這樣可以使后續(xù)的分析變得更加方便,在本次案例中,不需要選擇子集,所以可以先跳過(guò)這一步。
2、列重命名
有些數(shù)據(jù)的原始列命名對(duì)于數(shù)據(jù)分析來(lái)說(shuō)容易誤解,或者容易產(chǎn)生歧義,一不小心就理解錯(cuò)了,很不利于分析,在這個(gè)時(shí)候,就需要給列名重命名。
#把購(gòu)藥時(shí)間改成銷售時(shí)間,直接在原數(shù)據(jù)框進(jìn)行修改。
sales.rename(columns= {'購(gòu)藥時(shí)間':'銷售時(shí)間'},inplace=True)
#看一下修改后的數(shù)據(jù)
sales.head()

3、缺失數(shù)據(jù)處理
任何一個(gè)得到的數(shù)據(jù)都很有可能會(huì)有缺失值,那么對(duì)于這些缺失值一定需要處理一下,不然會(huì)干擾后來(lái)的分析結(jié)果。刪除缺失值用dropna函數(shù),代碼如下
#沒(méi)有時(shí)間和社??ㄌ?hào)的消費(fèi)數(shù)據(jù)對(duì)于本次分析是無(wú)效的,
#所以清理一下缺失值
sales = sales.dropna(subset=['銷售時(shí)間','社??ㄌ?hào)'],how='any')
4、數(shù)據(jù)類型處理
在導(dǎo)入的時(shí)候?yàn)榱朔乐褂行?shù)據(jù)導(dǎo)入不進(jìn)來(lái),所以強(qiáng)制所有數(shù)據(jù)都是object類型,但在實(shí)際分析上這樣是不可能的,所以要把需要改變類型的數(shù)據(jù)類型改變了,通過(guò)觀察,我們發(fā)現(xiàn),銷售數(shù)量,應(yīng)收金額,實(shí)收金額應(yīng)該改成float類型,銷售時(shí)間應(yīng)該清理后改成時(shí)間類型,對(duì)于改變成float類型的幾列,使用astype函數(shù),代碼如下。
這里因?yàn)榭瀑惥W(wǎng)導(dǎo)入的時(shí)候無(wú)法選擇object,所以跳過(guò)。
sales['銷售數(shù)量'] = sales['銷售數(shù)量'].astype('float64')
sales['應(yīng)收金額'] = sales['應(yīng)收金額'].astype('float64')
sales['實(shí)收金額'] = sales['實(shí)收金額'].astype('float64')
而銷售時(shí)間那一列,則需要進(jìn)行處理后才能轉(zhuǎn)換為時(shí)間類型,把銷售時(shí)間的日期和星期分開。
sales['銷售時(shí)間'], sales['銷售星期'] = sales['銷售時(shí)間'].str.split(' ', 1).str
#切分好之后,把銷售時(shí)間變?yōu)闀r(shí)間類型
sales['銷售時(shí)間'] = pd.to_datetime(sales['銷售時(shí)間'],format='%Y-%m-%d',errors='coerce')
#先看一下清洗到這個(gè)階段的數(shù)據(jù)
sales.head()

現(xiàn)在數(shù)據(jù)看起來(lái)已經(jīng)有點(diǎn)那么回事了,接下來(lái)把數(shù)據(jù)按照時(shí)間排序一下。排序之后索引會(huì)被打亂,所以也需要重置一下索引。代碼如下
#將數(shù)據(jù)按照銷售時(shí)間排序
sales = sales.sort_values('銷售時(shí)間',ascending=True)
#重置索引
sales = sales.reset_index(drop=True)
#再看一下數(shù)據(jù)
sales.head()

6、異常值處理
這里就要清洗前面理解數(shù)據(jù)時(shí)提到的負(fù)值的數(shù)據(jù)了,這些數(shù)據(jù)必須是要大于0才屬于正常數(shù)據(jù)。
從數(shù)據(jù)基本情況可以看出,銷售數(shù)量和應(yīng)收金額,實(shí)收金額,都有負(fù)的異常值,需要把這些值舍去,即選取銷售數(shù)量和應(yīng)收金額大于0的列,代碼如下
#選取銷售數(shù)量和應(yīng)收金額大于0的列
sales = sales[(sales['銷售數(shù)量'] > 0) & (sales['應(yīng)收金額'] > 0)]
#看一下目前的數(shù)據(jù)
sales.head()

做完以上幾個(gè)清洗的工作,接下來(lái)就可以開始正式的數(shù)據(jù)分析了。
四、數(shù)據(jù)分析
1 月均消費(fèi)次數(shù)
這里的月均消費(fèi)次數(shù)定義為總次數(shù)除以月份,其中假如一個(gè)人一天買了兩次藥,但只算做消費(fèi)了一次,即計(jì)算次數(shù)的時(shí)候需要進(jìn)行去重處理。
#首先對(duì)數(shù)據(jù)進(jìn)行一個(gè)去重,使用drop_duplicates函數(shù)
sales = sales.drop_duplicates(subset=['銷售時(shí)間','社保卡號(hào)'])
#去重后看一下一共有多少條數(shù)據(jù)
total = sales.shape[0]
這里可以看到去重后的數(shù)據(jù)一共5363條,相比較原始數(shù)據(jù)的6578條,已經(jīng)去除了1200多條重復(fù)數(shù)據(jù)。
#再計(jì)算月份
#用銷售時(shí)間的最大值減去最小值即可得到天數(shù),再除以(地板除)三十就可以得到月份了
month = (sales['銷售時(shí)間'].max() - sales['銷售時(shí)間'].min()).days // 30
KPI1 = total / month
print('月均消費(fèi)次數(shù)為:',KPI1)

2 月均消費(fèi)金額
同樣,月均消費(fèi)金額為總實(shí)收金額除以總月份,在計(jì)算總金額的時(shí)候不能去重,需要都計(jì)算上金額。
#計(jì)算總金額
sum_sale = sales['實(shí)收金額'].sum()
KPI2 = sum_sale / month
print('月均消費(fèi)金額為:',KPI2)

3 客單價(jià)
客單價(jià)就是總實(shí)收金額除以總消費(fèi)次數(shù)
kdj = sum_sale / total
print('客單價(jià)為:',kdj)

4 消費(fèi)趨勢(shì)
關(guān)于消費(fèi)趨勢(shì),首先我們先來(lái)看一下每天的消費(fèi)總金額的變化,把數(shù)據(jù)按天聚合,繪圖,代碼如下。
##對(duì)去重后的數(shù)據(jù)按照天進(jìn)行重新采樣
#首先要把索引變成時(shí)間
sales.index = pd.DatetimeIndex(sales['銷售時(shí)間'])
#將索引按天聚合
b = sales.resample('D').sum()
b.head()

#畫圖
plt.plot(b.index,b['實(shí)收金額'])
plt.xlabel = 'Time'
plt.ylabel = 'Money'
plt.title = '總金額消費(fèi)趨勢(shì)圖'
plt.show()

根據(jù)上圖可以看出每日消費(fèi)金額主要在1000-4000波動(dòng),波動(dòng)幅度較大,并且有幾個(gè)峰值特別高的日子。
#按月采樣
salesm = sales.resample('M').sum()
#畫圖
plt.plot(salesm.index, salesm['實(shí)收金額'])
plt.show()

這里看出該藥店的銷售總額和客流量基本成正比。二月份的客流量最少,同樣銷售業(yè)績(jī)也最差,同樣的四月份客流量最高,銷售總額也最多。
但是六月份客流量變少了,而消費(fèi)總額卻變多了,可能是因?yàn)槿司I的藥更貴了,具體原因尚且不得而知,七月份的數(shù)據(jù)如此小的原因是因?yàn)槠咴路葜唤y(tǒng)計(jì)了半個(gè)月的數(shù)據(jù),數(shù)據(jù)不全,不能拿來(lái)做比較。

通過(guò)這里,我們可以發(fā)現(xiàn),周五周六的銷售總額要顯著的的高于其他日期,即周五周六應(yīng)該前來(lái)買藥的人更多,銷售的藥品更多。
即每周的銷售趨勢(shì)是周日到周四銷售總額會(huì)有波動(dòng),但是幅度不大,周五周六的銷售總額相對(duì)較高,按月份比較的話,四月份的銷售總額顯著的高,而二月份的銷售總額顯著的低,猜測(cè)銷售總額非常低是因?yàn)榇汗?jié)的緣故。
五、反思與總結(jié)
本次數(shù)據(jù)分析過(guò)程中遇到一個(gè)問(wèn)題,耗費(fèi)了許多時(shí)間。
問(wèn)題便是在最后的兩個(gè)趨勢(shì)圖上,由于數(shù)據(jù)和題目都是科賽網(wǎng)拿到的,所以一開始在做每月消費(fèi)金額趨勢(shì)圖的時(shí)候代碼如下:
#畫圖之前的代碼全部一致
b.plot(x = b.index,y = '實(shí)收金額')
plt.xlabel = 'Time'
plt.ylabel = 'Money'
plt.title = '總金額消費(fèi)趨勢(shì)圖'
plt.show()
就是這樣的代碼,與科賽網(wǎng)的代碼一致,但是卻無(wú)法正常運(yùn)行,報(bào)錯(cuò)如下:
---------------------------------------------------------------------------
KeyError Traceback (most recent call last)
<ipython-input-90-25f586ca3e93> in <module>()
1 #畫圖
2 #plt.plot(b.index,b['實(shí)收金額'])
----> 3 b.plot(x = b.index,y = '實(shí)收金額',style='-')
4 plt.xlabel = 'Time'
5 plt.ylabel = 'Money'
D:\IT\Anaconda5\lib\site-packages\pandas\plotting\_core.py in __call__(self, x, y, kind, ax, subplots, sharex, sharey, layout, figsize, use_index, title, grid, legend, style, logx, logy, loglog, xticks, yticks, xlim, ylim, rot, fontsize, colormap, table, yerr, xerr, secondary_y, sort_columns, **kwds)
2939 fontsize=fontsize, colormap=colormap, table=table,
2940 yerr=yerr, xerr=xerr, secondary_y=secondary_y,
-> 2941 sort_columns=sort_columns, **kwds)
2942 __call__.__doc__ = plot_frame.__doc__
2943
D:\IT\Anaconda5\lib\site-packages\pandas\plotting\_core.py in plot_frame(data, x, y, kind, ax, subplots, sharex, sharey, layout, figsize, use_index, title, grid, legend, style, logx, logy, loglog, xticks, yticks, xlim, ylim, rot, fontsize, colormap, table, yerr, xerr, secondary_y, sort_columns, **kwds)
1975 yerr=yerr, xerr=xerr,
1976 secondary_y=secondary_y, sort_columns=sort_columns,
-> 1977 **kwds)
1978
1979
D:\IT\Anaconda5\lib\site-packages\pandas\plotting\_core.py in _plot(data, x, y, subplots, ax, kind, **kwds)
1764 if is_integer(x) and not data.columns.holds_integer():
1765 x = data_cols[x]
-> 1766 elif not isinstance(data[x], ABCSeries):
1767 raise ValueError("x must be a label or position")
1768 data = data.set_index(x)
D:\IT\Anaconda5\lib\site-packages\pandas\core\frame.py in __getitem__(self, key)
2680 if isinstance(key, (Series, np.ndarray, Index, list)):
2681 # either boolean or fancy integer index
-> 2682 return self._getitem_array(key)
2683 elif isinstance(key, DataFrame):
2684 return self._getitem_frame(key)
D:\IT\Anaconda5\lib\site-packages\pandas\core\frame.py in _getitem_array(self, key)
2724 return self._take(indexer, axis=0)
2725 else:
-> 2726 indexer = self.loc._convert_to_indexer(key, axis=1)
2727 return self._take(indexer, axis=1)
2728
D:\IT\Anaconda5\lib\site-packages\pandas\core\indexing.py in _convert_to_indexer(self, obj, axis, is_setter)
1325 if mask.any():
1326 raise KeyError('{mask} not in index'
-> 1327 .format(mask=objarr[mask]))
1328
1329 return com._values_from_object(indexer)
KeyError: "DatetimeIndex(['2018-01-01', '2018-01-02', '2018-01-03', '2018-01-04',\n '2018-01-05', '2018-01-06', '2018-01-07', '2018-01-08',\n '2018-01-09', '2018-01-10',\n ...\n '2018-07-10', '2018-07-11', '2018-07-12', '2018-07-13',\n '2018-07-14', '2018-07-15', '2018-07-16', '2018-07-17',\n '2018-07-18', '2018-07-19'],\n dtype='datetime64[ns]', name='銷售時(shí)間', length=200, freq=None) not in index"
可以說(shuō)我為了解決這個(gè)問(wèn)題耗費(fèi)了小半天的時(shí)間,因?yàn)榭瀑惥W(wǎng)的源代碼我復(fù)制了一遍運(yùn)行也是出錯(cuò)的,所以個(gè)人估計(jì)是matplotlib庫(kù)的版本問(wèn)題?具體原因我也不是很確定。
在百度搜了很久沒(méi)有相關(guān)結(jié)果,后來(lái)還是在stackoverflow找到了嘗試的代碼。

這才有了第四部分的正確代碼和趨勢(shì)圖。
另外還有一點(diǎn)就是科賽網(wǎng)的最后兩個(gè)趨勢(shì)圖個(gè)人覺得選取的數(shù)據(jù)不對(duì),因?yàn)樵创a對(duì)日期重采樣之后不是以sum計(jì)算的而是以count計(jì)算的。代碼如下:
##對(duì)去重后的數(shù)據(jù)按照天進(jìn)行重新采樣
#首先要把索引變成時(shí)間
sales.index = pd.DatetimeIndex(sales['銷售時(shí)間'])
#然后對(duì)其按照每天從新采樣
salesd = sales.resample('D').count()
#畫圖
salesd.plot(x = salesd.index, y = '實(shí)收金額')
plt.xlabel('Time')
plt.ylabel('Money')
plt.title('xiao shou shu ju')
plt.show()
對(duì)此我特意將代碼改成count然后把數(shù)據(jù)展開查看

所以按照代碼來(lái)看,源代碼畫圖的數(shù)據(jù)是X為索引-銷售時(shí)間,y為實(shí)收金額,但是怎么看這樣的實(shí)收金額都不會(huì)是一天的實(shí)收金額。因?yàn)閷?shí)際上數(shù)字21是2018年1月1日銷售了21單,而實(shí)際的實(shí)收金額則需要重采樣之后按照sum來(lái)計(jì)算。
后面兩個(gè)趨勢(shì)圖都是這樣的情況,就趨勢(shì)圖來(lái)說(shuō)我相信自己這種做法才是正確的。
但是對(duì)于畫圖的plot()的代碼我還是只知其然不知其所以然,希望有大佬能夠指點(diǎn)迷津。
以上。
謝謝。