前两节内容已介绍如何清理数据,以及常用的分析模型。
本文会重点介绍pandas分析功能应用,包括:
多层索引、结构重塑、合并关联、交叉透视、分组聚合、时间序列。
然后以一个完整门店型业务项目,演示如何应用功能完成数据分析任务。

行和列的多层索引

在介绍功能前,先通过案例快速理解一个关键概念:多层索引
在新媒体运营工作中,我们需要记录每篇文章的阅读量,而阅读量主要由标题和发送渠道有关。
我们可以用两种形式来记录数据:
6、数据分析 - 图1
其中第1种更简洁,也经常是首先;第二种比较臃肿,标题名被重复记录。
但如果在此基础上,增加工作组维度,即X、Y、Z三个组分别做同样的事,该如何记录?
第1种形式中已经无法用单个表格表示,只能增加第2、第3张表来表示不同工作组;
但第2种形式可以完美解决,只需要增加一列“工作组”即可,如上图右下所示。
如果再想增加更多维度,比如增加多个自媒体账号;或者想再增加更多数据,比如点赞和转发等,该如何记录呢?
可以参考第2种形式,把工作组和账号从列改成行,用3列记录阅读、点赞、转发数据;
也可以把现有列扩充成2个维度:工作组和账号,一个工作组下分为多个账号,然后在各个账号下增加阅读、点赞、转发的数据。
6、数据分析 - 图2
可以看到,同样的数据,可以通过行和列之间转换,呈现出不同的形态。
在数据分析过程中,常需汇总不同维度的数据,或关联对比多维度数据间的关系。

Pandas分析核心功能

pandas提供了多层次索引结构,处理多维度数据非常方便。
上面演示的多维度表格数据,就是用pandas随机生成和处理。

  1. import pathlib
  2. import numpy as np
  3. import pandas as pd
  4. from pandas import MultiIndex as MI
  5. path = list(pathlib.Path.cwd().parents)[1].joinpath('data/dataproc/006analysis')
  6. excel_A_path = path.joinpath('testA.xlsx')
  7. excel_B_path = path.joinpath('testB.xlsx')
  8. excel_C_path = path.joinpath('testC.xlsx')
  9. excel_D_path = path.joinpath('testD.xlsx')
  10. excel_E_path = path.joinpath('testE.xlsx')
  11. excel_F_path = path.joinpath('testF.xlsx')
  12. excel_G_path = path.joinpath('testG.xlsx')
  13. # 定义维度列表
  14. channel_list = ['渠道A','渠道B','渠道C']
  15. title_list = ['标题1','标题2','标题3','标题4','标题5']
  16. group_list = ['工作组X','工作组Y','工作组Z']
  17. account_list = ['公众号','头条号','知乎','小红书']
  18. # 定义某个新媒体工作组在多个渠道下多篇文章标题测试数据
  19. team1 = pd.Series(np.random.randint(100,1000,15),
  20. index=MI.from_product([channel_list,title_list],
  21. names=['渠道','标题']),
  22. name='工作组X')
  23. # 导出Excel表
  24. team1.unstack().to_excel(excel_A_path)
  25. team1.to_excel(excel_B_path)
  26. # 定义更多工作组
  27. team2 = pd.Series(np.random.randint(100,1000,15),
  28. index=MI.from_product([channel_list,title_list],
  29. names=['渠道','标题']),
  30. name='工作组Y')
  31. # 合并两个Series到DataFrame
  32. df = pd.concat([team1, team2], axis=1)
  33. df.to_excel(excel_C_path)
  34. # 增加账号维度,和工作组一起并入行内
  35. df = pd.DataFrame(np.random.randint(100,2000,(180,3)),
  36. index=MI.from_product([channel_list, title_list, group_list,account_list],
  37. names=['渠道','标题','工作组','账号']),
  38. columns=['阅读量','点赞量','转发量'])
  39. df.to_excel(excel_D_path)
  40. # 把工作组和账号放到列
  41. df.stack().unstack('工作组').unstack('账号').unstack().to_excel(excel_E_path)
  42. # 只把账号维度放到列
  43. df_result = df.stack().unstack('账号').unstack()
  44. df_result.to_excel(excel_F_path)
  45. # 调整下行内各维度顺序
  46. df_result.index=MI.from_product([group_list,channel_list,title_list],
  47. names=['工作组','渠道','标题'])
  48. df_result.to_excel(excel_G_path)

其中,用到了2个核心功能:结构重塑、合并关联,此外通过to_excel导出xlsx文件方便截图。

结构重塑

pandas中,Series是1维结构,包含1维的索引;DataFrame是2维结构,包含行和列两个维度索引。DataFrame可以看成是由多个Series共享行索引后的组合体,如上述案例中用concat方法把两个Series合并成1个DataFrame
DataFrame在行和列维度,都可以有多层索引,并且可以用stackunstack方法转换行列维度。
还有4个常用方法用于设置行列索引:reset_indexset_indexTmelt

  1. # 把所有行索引转为列索引
  2. df = df_result.reset_index()
  3. # 设置行索引
  4. df.set_index(['工作组','渠道','标题'])
  5. # melt选择部分id列,其他列转为行数据放在id列后
  6. df.melt(id_vars=['工作组','渠道','标题'])
  7. # 行和列转换
  8. df.T

固定数据结构后,就可以用索引、筛选、切片等方式访问数据了。

  1. # 获取行索引
  2. df.index
  3. # 获取列索引
  4. df.columns
  5. # 按列索引
  6. print(df[('头条号','阅读量')])
  7. # 按列的某个level索引
  8. print(df['头条号'])
  9. # 按列索引,效果相同
  10. print(df['头条号']['阅读量'])
  11. # 按行level索引
  12. df.loc['工作组X']
  13. # 按行多层索引
  14. df.loc[('工作组X','渠道A')]
  15. df.loc[('工作组X','渠道A','标题1')]
  16. # 按行列索引
  17. df.loc[('工作组X','渠道A','标题1')][('头条号')]
  18. df.loc[('工作组X','渠道A','标题1')][('头条号','阅读量')]
  19. # 指明某个维度索引
  20. # 按行索引
  21. df.loc(axis=0)['工作组X',:,['标题1','标题3']]
  22. # 按列索引
  23. df.loc(axis=1)[['公众号','头条号'],['阅读量','转发量']]
  24. # 借助切片器索引
  25. idx = pd.IndexSlice
  26. df.loc[idx['工作组X', :, ['标题1', '标题3']], idx['公众号':,'阅读量']]
  27. # 借助xs交叉选取,任意选取某个层级索引
  28. # 按行
  29. df.xs('标题1',level='标题')
  30. # 按列
  31. df.xs('阅读量',level=1,axis=1)
  32. # 行列交叉
  33. df.xs('渠道A',level='渠道').xs('阅读量',level=1,axis=1)

合并关联

pandas用于合并关联数据的操作主要有4种:

  1. concat,可以在行和列上拼接数据,支持innerouter两种连接模式,支持不同维度数据连接;
  2. appendconcat的简化版,方便向列和行尾部追加数据;
  3. merge,在列维度按某个key合并数据,和SQL数据库的JOIN操作相似,支持innerouterrightleft4种连接模式;
  4. join,当key正好是索引时merge方法的特例,其内部用merge实现。
    1. import numpy as np
    2. import pandas as pd
    3. df1 = pd.DataFrame({'A': ['A0', 'A1', 'A2', 'A3'],
    4. 'B': ['B0', 'B1', 'B2', 'B3'],
    5. 'C': ['C0', 'C1', 'C2', 'C3'],
    6. 'D': ['D0', 'D1', 'D2', 'D3']},
    7. index=range(4))
    8. df2 = pd.DataFrame({'A': ['A4', 'A5', 'A6', 'A7'],
    9. 'B': ['B4', 'B5', 'B6', 'B7'],
    10. 'C': ['C4', 'C5', 'C6', 'C7'],
    11. 'D': ['D4', 'D5', 'D6', 'D7']},
    12. index=range(4,8))
    13. df3 = pd.DataFrame({'A': ['A8', 'A9', 'A10', 'A11'],
    14. 'B': ['B8', 'B9', 'B10', 'B11'],
    15. 'C': ['C8', 'C9', 'C10', 'C11'],
    16. 'D': ['D8', 'D9', 'D10', 'D11']},
    17. index=range(8,12))
    18. df4 = pd.DataFrame({'B': ['B2', 'B3', 'B6', 'B7'],
    19. 'D': ['D2', 'D3', 'D6', 'D7'],
    20. 'F': ['F2', 'F3', 'F6', 'F7']},
    21. index=[2, 3, 6, 7])
    22. # 按行叠加,可以选择增加一层行索引,比如表示数据来自哪个数据库
    23. df_concat = pd.concat([df1,df2,df3], keys=['X','Y','Z'])
    24. # 也可以用dict传递,效果相同
    25. df_concat_0 = pd.concat({'X': df1, 'Y': df2, 'Z': df3})
    26. # 按列叠加,行索引默认按outer并集,默认填充NaN
    27. df_concat_1 = pd.concat([df1, df4], axis=1, sort=False)
    28. # 按列叠加,行索引按inner交集
    29. df_concat_2 = pd.concat([df1, df4], axis=1, join='inner')
    30. # 用append追加数据,但不能增加行索引
    31. df_concat_3 = df1.append(df2).append(df3)
    32. df_concat_4 = df1.append([df2, df3])
    33. # 按列追加,列不完全一致时会增加行
    34. df_concat_5 = df1.append(df4)
    35. # 按列追加,忽略行索引,已有数据不会被覆盖
    36. df_concat_6 = df1.append(df4,ignore_index=True)
    37. # 类数据库SQL的合并操作
    38. left = pd.DataFrame({'key1': ['K0', 'K1', 'K2', 'K3'],
    39. 'key2': ['K0', 'K1', 'K0', 'K1'],
    40. 'A': ['A0', 'A1', 'A2', 'A3'],
    41. 'B': ['B0', 'B1', 'B2', 'B3']})
    42. right = pd.DataFrame({'key1':['K0', 'K1', 'K2', 'K3'],
    43. 'key2': ['K0', 'K0', 'K0', 'K0'],
    44. 'C': ['C0', 'C1', 'C2', 'C3'],
    45. 'D': ['D0', 'D1', 'D2', 'D3']})
    46. # 列合并,默认用inner连接模式,即key同时出现在两组数据时包含该key对应行数据
    47. df_merge = pd.merge(left, right, on='key1')
    48. # 用两列key,inner连接模式,必须同时存在key1和key2才会包含在结果中
    49. df_merge_inner = pd.merge(left, right, on=['key1','key2'])
    50. # left连接模式,以left内(key1,key2)为键,right内没有的数据填NaN
    51. df_merge_left = pd.merge(left, right, how='left', on=['key1','key2'])
    52. # right连接模式,以right内(key1,key2)为键
    53. df_merge_right = pd.merge(left, right, how='right', on=['key1','key2'])
    54. # outer连接模式,包含left和right内所有(key1,key2)键组合
    55. df_merge_outer = pd.merge(left, right, how='outer', on=['key1','key2'])
    56. pd.merge(left, right, how='inner', on=['key1', 'key2'])
    57. # 当key正好是索引时,可以用merge的简化版:join
    58. left = pd.DataFrame({'A': ['A0', 'A1', 'A2'],
    59. 'B': ['B0', 'B1', 'B2']},
    60. index=['K0', 'K1', 'K2'])
    61. right =pd.DataFrame({'C': ['C0', 'C2', 'C3'],
    62. 'D': ['D0', 'D2', 'D3']},
    63. index=['K0', 'K2', 'K3'])
    64. # join默认left连接模式
    65. df_join_left = left.join(right)
    66. # 等价的merge操作
    67. df_join_left_0 = pd.merge(left, right, left_index=True, right_index=True, how='left')
    68. # right连接
    69. df_join_right = left.join(right, how='right')
    70. df_join_right_0 = pd.merge(left, right, left_index=True, right_index=True, how='right')
    71. # outer连接
    72. df_join_outer = left.join(right, how='outer')
    73. df_join_outer_0 = pd.merge(left, right, left_index=True, right_index=True, how='outer')
    74. # inner连接
    75. df_join_inner = left.join(right, how='inner')
    76. df_join_inner_0 = pd.merge(left, right, left_index=True, right_index=True, how='inner')
    关于数据合并的4个方法:
  • concatappend相对容易理解,常用于合并多个数据源。
  • merge可以理解为pandas在内存中执行SQL连接操作,功能强大但使用相对复杂;join使用相对更频繁,也更易用。

初学者只需了解4个方法使用场景,掌握常见用法即可。对于复杂情况,可在使用时参考官方文档应用。

交叉透视

变换数据结构,有助于发现各维度数据间的关系。
数据结构整理好后,我们可以通过透视表和分组统计等功能,对数据展开分析。
比如,我们想了解“渠道和标题对头条账号文章数据的影响”:

  1. # 把数据所有维度都变成列
  2. df = df_result.stack().reset_index()
  3. # 查看渠道和标题对头条号的影响
  4. df_pv = df.pivot_table(index=['渠道','标题'],columns=['文章数据'],values=['头条号'],aggfunc=[np.mean])

首先,我们把数据还原为列,然后通过pivot_table方法从数据生成透视表。
pandas透视表效果和Excel类似,都可以方便观察不同维度数据间的关系。
6、数据分析 - 图3
indexcolumn对应的数据值唯一时,可以用简化的pivot方法,省去用aggfunc聚合。
此外,也可以通过crosstab函数快速交叉对比2个序列数据关系,它默认统计数据出现频率。

  1. df_s = pd.DataFrame({
  2. 'A':['A1','A2','A3'],
  3. 'B':['B1','B2','B3'],
  4. 'C':['C1','C2','C3'],
  5. 'D':['D1','D2','D3']
  6. })
  7. # 当index和column应对的数据唯一时,可用简化的pivot方法
  8. df_s.pivot(index='A',columns='B',values='C')
  9. # 交叉对比任意两个Series间数据关系,不要求是DataFrame,默认aggfunc统计频率
  10. pd.crosstab(index=df_s['A'],columns=df_s['B'])
  11. # 单层交叉
  12. pd.crosstab(index=df['标题'],columns=df['文章数据'],values=df['头条号'],aggfunc=np.mean,margins=True,margins_name='总计')
  13. # 多层交叉
  14. pd.crosstab(index=[df['渠道'],df['标题']],columns=df['文章数据'],values=df['头条号'],aggfunc=np.mean)

分组聚合

分组聚合,就是先把数据分为多个组,然后对各组进行计算,最后把各组计算结果合并到一起。
比如,为了统计“各工作组的文章总阅读量”,可以按3步计算:

  1. 把数据按工作组划分成X、Y、Z三组;
  2. 分别统计3个工作组的文章总阅读量,包括各渠道、账号和标题;
  3. 最后,输出每个工作组对应的阅读量。

pandas计算非常方便:首先用IndexSlice对多层索引切片,筛选出“阅读量”所在列,再以“工作组”分组,然后用sum统计出各个账号阅读量的总和,最后在列维度用sum(axis=1)计算各个账号阅读量总和。

  1. idx = pd.IndexSlice
  2. # 统计各个工作组的总阅读量
  3. df.loc[:,idx[:,'阅读量']].groupby(level='工作组').sum().sum(axis=1)
  4. # 统计各组在各渠道下总阅读量
  5. df.loc[:,idx[:,'阅读量']].groupby(['工作组','渠道']).sum()

groupby返回的是一个GroupBy对象,它有一个groups属性,包含着每个分组名和对应的索引。

常见的分组方式有4种:

  1. 先过滤再分组,就像上面使用的;
  2. 先分组,再过滤,在GroupBy中过滤出所需要的列;
  3. 以标签形式过滤,可以把列打上不同标记进行统计;
  4. 以函数分组,函数会被作用在每个分组列。

    1. df = df_result
    2. idx = pd.IndexSlice
    3. # 统计各个工作组的总阅读量
    4. # 方式1: 先过滤再分组
    5. df.loc[:,idx[:,'阅读量']].groupby(level='工作组').sum().sum(axis=1)
    6. # 统计各个渠道的总阅读量
    7. df.loc[:,idx[:,'阅读量']].groupby('渠道').sum().sum(axis=1)
    8. # 统计各组在各渠道下总阅读量
    9. df.loc[:,idx[:,'阅读量']].groupby(['工作组','渠道']).sum()
    10. # 不用分组生成索引
    11. df.loc[:,idx[:,'阅读量']].groupby('工作组', as_index=False).sum()
    12. # groupby返回的是GroupBy对象
    13. grp_by = df.loc[:,idx[:,'阅读量']].groupby('工作组')
    14. grp_by.groups # 返回一个dict对象
    15. # 方式2: 先分组,再过滤
    16. df.groupby('工作组')[[('公众号', '阅读量')]].sum()
    17. df.groupby('工作组')[MI.from_product([account_list,['阅读量']])].sum().sum(axis=1)
    18. # 方式3: 以标签形式过滤,传入一个dict,聚合所需列
    19. mapping = {c:c[1] for c in list(df.columns)}
    20. # 统计各标题总的文章数据
    21. df.groupby(mapping,axis=1).sum()
    22. # 方式4: 以函数分组,函数会应用在每个分组列
    23. # 如各标题各账号数据总和
    24. df.groupby(lambda x: x[1],axis=1).sum()
    25. # 像dict一样迭代GroupBy对象
    26. for name, group in grp_by:
    27. print(name)
    28. # 多层索引下可以多层分组形式迭代
    29. for (k1,k2), group in df.groupby(['工作组','渠道']):
    30. print(k1,k2)
    31. # 把GroupBy转为dict
    32. pieces = dict(list(df.groupby('工作组')))
    33. pieces['工作组X']
    34. # 获取某个组
    35. groupx = grp_by.get_group('工作组X')

    分组后,可以按组进行聚合统计,主要有3种方式:

  5. 直接在GroupBy对象上调用sum等统计方法;

  6. 通过aggregate方法(或agg缩写)指定统计函数;
  7. 通过apply自定义对每个分组数据处理。
    1. # 数据基本简述统计
    2. grp_by.describe()
    3. # 在GroupBy对象上应用聚合类统计函数
    4. grp_by.aggregate(np.mean) # 算数平均
    5. # 应用多个聚合函数,agg是aggregate缩写
    6. grp_by.agg([np.min, np.max, np.mean])
    7. grp_by.agg([np.min, np.max, np.mean]).rename(
    8. columns={'amin': '最小值','amax': '最大值','mean':'算数平均'})
    9. # 在不同列应用不同聚合统计函数
    10. # 如果是多层次索引,先扁平化再处理
    11. df_flat = df.stack().reset_index()
    12. df_flat[df_flat['文章数据']=='阅读量'].groupby('工作组').agg(
    13. {'公众号':np.min,'头条号':np.max,'知乎':np.mean,'小红书':np.median})
    14. # 或者动态生成不同统计函数dict
    15. agg_dict = {}
    16. agg_calc = [np.min, np.max, np.mean, np.median]
    17. for ac, calc in zip(account_list, agg_calc):
    18. agg_dict.update({col:calc
    19. for col in df.columns[
    20. (df.columns.get_level_values(0)==ac) &
    21. (df.columns.get_level_values(1)=='阅读量')
    22. ]})
    23. grp_by.agg(agg_dict)
    24. # 使用自定义聚合函数,如统计最大最小值的差
    25. grp_by.agg(lambda x: x.max()-x.min())
    26. # 使用更通用的方法处理各个分组数据:apply
    27. grp_by.apply(lambda x: x.describe())
    28. # 显示各组的各账号阅读量最高的标题
    29. max_title_f = lambda x: x.groupby('标题').max().max()
    30. # 显示标题阅读量
    31. grp_by.apply(max_title_f)
    32. max_title_f2 = lambda x: x.unstack().stack('账号').groupby('账号').max().idxmax(axis=1)
    33. # 显示哪个标题
    34. grp_by.apply(max_title_f2)
    35. # 统计文章数据最优标题
    36. df.stack().groupby(['工作组','文章数据']).apply(lambda x:x.unstack('标题').max().idxmax(axis=1))

时间序列

在数据分析中,时间是一个重要维度,比如:按年/季/月统计销量、同比/环比增长率等。
Python内置了2个模块处理时间:datetimetime(处理时间戳)。

  1. import time
  2. from datetime import datetime
  3. # 当前时间
  4. now = datetime.now()
  5. print(now.year, now.month, now.day, now.hour, now.minute, now.second, now.microsecond)
  6. # 时间差
  7. delta = datetime(2020, 9, 1) - datetime(2020, 8, 1, 10, 10, 10)
  8. print(delta.days, delta.seconds, delta.microseconds)
  9. # 转为字符串
  10. print(str(now))
  11. print(now.strftime('%Y-%m-%d'))
  12. # 从字符串转回datatime数据
  13. print(type(datetime.strptime('2020-10-1', '%Y-%m-%d')))
  14. # 获取当前时间的时间戳,时间戳是个数字
  15. now_ts = time.time()
  16. # datetime 转 时间戳
  17. print(now.timestamp())
  18. # 时间戳转为datetime
  19. print(datetime.fromtimestamp(now_ts))

在实际项目中,为了增强时间数据处理能力,可以借助三方模块dateutil

  • 安装:pip install python-dateutil
    1. from dateutil.parser import parse
    2. from dateutil import tz, zoneinfo
    3. from dateutil.rrule import rrule, MONTHLY,DAILY,WEEKLY,SU
    4. # 从文字解析
    5. print(parse('Wed'), parse('Sep 12'), parse('2020-08-01'))
    6. print(parse('Today is January 1, 2047 at 8:21:00AM', fuzzy_with_tokens=True))
    7. parse('2020-02-24T20:30:20+08:00')
    8. # 获取所有时区
    9. zonefile = zoneinfo.get_zonefile_instance()
    10. zonefile.zones.keys()
    11. # 获取上海时区当前时间
    12. tz_sh = tz.gettz('Asia/Shanghai')
    13. now_sh = datetime.now(tz=tz_sh)
    14. # 时间段生成
    15. start_date = datetime(2020, 1, 1)
    16. # 从start_date开始连续生成4个月的首日
    17. list(rrule(freq=MONTHLY, count=4, dtstart=start_date))
    18. # 从start_date开始连续生成10天
    19. list(rrule(freq=DAILY, count=10, dtstart=start_date))
    20. # 生成两个时间之间的所有周日
    21. list(rrule(WEEKLY,byweekday=(SU),dtstart=parse('2020-01-01'),until=parse('2020-12-31')))

pandas提供了3种时间索引:DatetimeIndexTimedeltaIndexPeriodIndex

  1. import pandas as pd
  2. # DatetimeIndex类型序列
  3. # 生成连续的时间,默认频率是天
  4. pd.date_range('2020-01-01', '2020-06-30')
  5. # 生成20天的序列
  6. pd.date_range(start='2020-04-01', periods=20)
  7. # 生成每月最后一天
  8. pd.date_range('2020-01-01', '2020-12-31', freq='M')
  9. # 生成每月最后一个工作日
  10. pd.date_range('2020-01-01', '2020-12-31', freq='BM')
  11. # 生成4小时频率生成时间
  12. pd.date_range('2020-01-01', '2020-01-02', freq='4h')
  13. # 生成每月第三个周五
  14. pd.date_range('2020-01-01', '2020-06-30', freq='WOM-3FRI')
  15. # Timedelta类型
  16. pd.Timedelta(days=3, hours=4)
  17. td = pd.Timedelta('31 days 5 min 3 sec')
  18. print(td.days, td.seconds, td.microseconds)
  19. pd.timedelta_range(start='1 days', periods=5)
  20. # 比如每隔1小时生成100个打点序列
  21. s = pd.Series(np.arange(100),index=pd.timedelta_range('1 days', periods=100, freq='h'))
  22. # 再按天统计打点平均值
  23. s.resample('D').mean()
  24. # PeriodIndex类型序列
  25. # 月度时间
  26. pd.period_range('2020-01-01', '2020-06-30', freq='M')
  27. # 季度时间
  28. p_q = pd.PeriodIndex(['2020Q1', '2020Q2', '2020Q3'], freq='Q-DEC')
  29. # 转为月度时间,首月和最后月
  30. p_q.asfreq('M', 'start')
  31. p_q.asfreq('M', 'end')
  32. # 年度时间,以12月作为结束的一整年
  33. p_y = pd.period_range('2006', '2009', freq='A-DEC')
  34. # 转为每年最后一个工作日
  35. p_y.asfreq('B', how='end')

其中,主要处理方法有3个:

  • to_period:改变显示单位,但不做统计,数据量不变;
  • asfreq:重塑间隔单位,按单位压缩数据量;
  • resample:统计时间段内数据,聚合计算。
    1. import numpy as np
    2. import pandas as pd
    3. s = pd.Series(np.random.randint(0,100,1000),
    4. index=pd.date_range('2020-01-01',periods=1000,freq='H'))
    5. print(type(s.index)) # DatetimeIndex
    6. # 按天/月/季/年显示,但不统计,数量不变
    7. s.to_period('D') # 天
    8. s.to_period('M') # 月
    9. s.to_period('Q') # 季
    10. s.to_period('A') # 年
    11. type(s.to_period('A').index) # PeriodIndex
    12. # 按天显示,数量减少
    13. s.asfreq('D')
    14. s.asfreq('M')
    15. type(s.asfreq('M').index) # DatetimeIndex
    16. # 按日/月/季/年统计
    17. s.resample('D').sum().to_period('D')
    18. s.resample('M').sum().to_period('M')
    19. s.resample('Q').sum().to_period('Q')
    20. s.resample('A').sum().to_period('A')
    21. # 按时间字符串过滤
    22. s['2020-01']
    时间序列在业务分析中,主要用于观察数据增长趋势,或间隔数据统计,如年度/季度/月度等。

    门店型业务分析实战

    还是那句话:数据分析必须回归业务,第一步就是设定分析目标。
    根据上一节介绍的门店型业务分析重点,制定具体分析目标:
  1. 门店经营维度:单店日/月订单量和营收,全国门店年度营收排名。
  2. 用户运营维度:用FRM模型划分用户等级。
  3. 产品服务维度:单店畅销/滞销产品,全国TOP10畅销产品。

下面就以最常见的奶茶连锁加盟店作为分析对象,完成上面3个分析目标。

源数据格式介绍

大部分门店型业务品牌在早期开展业务时,主要借助POS机收银完成交易闭环,较少具备全国门店统一分析能力,数据需要从POS系统导出。
本案例会根据实战项目数据结构,模拟生成各门店导出的交易数据。
image.gif
其中1张门店清单表,记录了门店基本信息;N张主订单表和副订单明细表记录了每个门店订单数据。

  • 主订单描述了“谁在什么时候哪个店消费了多少钱”;
  • 副订单表描述了“每个订单具体包括哪些产品及其数量”。

具体数据在学习群获取,可以直接用生成好的数据,也可以用Notebook生成自己的数据。

门店经营分析

通过pandas的时间索引,可以很方便统计时间序列的数据。

  1. # 单个门店,以订单时间为索引
  2. df_shop = df_shop.set_index('订单日期')
  3. # 按日/月/季/年统计
  4. df_shop.resample('D')['实付'].sum().to_period('D') # 日营收
  5. df_shop.resample('M')['实付'].sum().to_period('M') # 月营收
  6. df_shop.resample('Q')['实付'].sum().to_period('Q') # 季营收
  7. df_shop.resample('A')['实付'].sum().to_period('A') # 年营收
  8. df_shop.resample('D')['订单ID'].count().to_period('D') # 日单量
  9. df_shop.resample('M')['订单ID'].count().to_period('M') # 月单量

单门店和多门店统计方式一致,当我们从多个表中加载完数据,可以用concat方法合并成一个大的DataFrame操作。

  1. # 全国门店
  2. df_shop_list = []
  3. df_shop_x_list = []
  4. for i in range(10):
  5. print(f'Reading SP{i:04d}...')
  6. df_shop_x_list.append(pd.read_excel(path.joinpath(f'SP{i:04d}_X.xlsx')))
  7. df_shop_list.append(pd.read_excel(path.joinpath(f'SP{i:04d}.xlsx')))
  8. print(len(df_shop_list), len(df_shop_x_list))
  9. # 合并成大表
  10. df_shops = pd.concat(df_shop_list, ignore_index=True)
  11. df_shops_x = pd.concat(df_shop_x_list, ignore_index=True)
  12. # 调整索引
  13. df_shops.set_index('订单日期', inplace=True)
  14. del df_shops['Unnamed: 0']
  15. # 统计历年全国门店年度营收
  16. s_all_ym = df_shops.reset_index().groupby([pd.Grouper(key='订单日期',freq='A'),
  17. pd.Grouper(key='门店ID')
  18. ])['实付'].sum().unstack('门店ID').to_period('A').stack('门店ID')
  19. # 历年来单店年营收排名
  20. df_all_ym = pd.DataFrame({'年营收':s_all_ym})
  21. df_all_ym.sort_values(by='年营收')
  22. df_all_ym['全对比排名']=df_all_ym['年营收'].rank(ascending=False)
  23. df_all_ym['按年排名']=df_all_ym.groupby(level=0, as_index=False).apply(lambda x: x['年营收'].rank(ascending=False)).droplevel(0)
  24. # 全国门店年度营收统计
  25. df_shops.groupby('门店ID')['实付'].apply(lambda x: x.resample('A').sum().to_period('A'))

用户运营分析

通过时间维度的聚合,可以很方便观察用户消费频率和金额。

  1. # 近半年消费过的用户
  2. s = df_shop.loc['2020-01-01':'2020-06-30']['用户ID'].value_counts()
  3. # 按客户维度统计:首次/最后一次消费时间,近Q/半年/1年消费次数
  4. grp_user = df_shop.reset_index().groupby('用户ID')
  5. grp_user_q = df_shop.loc['2020-04-01':'2020-06-30']
  6. grp_user_h = df_shop.loc['2020-01-01':'2020-06-30']
  7. grp_user_y = df_shop.loc['2019-07-01':'2020-06-30']
  8. df_user_rf = pd.DataFrame({
  9. '首次消费':grp_user.first()['订单日期'],
  10. '最后一次消费':grp_user.last()['订单日期'],
  11. '近Q消费次数':grp_user_q['用户ID'].value_counts(),
  12. '近半年消费次数':grp_user_h['用户ID'].value_counts(),
  13. '近1年消费次数':grp_user_y['用户ID'].value_counts(),
  14. '截止目前总消费次数':df_shop['用户ID'].value_counts()}).fillna(0)
  15. # 统计:总消费金额、近Q/半年/1年消费金额
  16. df_user_m = pd.DataFrame({
  17. '累计总消费金额':grp_user['实付'].sum(),
  18. '近Q消费金额':grp_user_q.groupby('用户ID')['实付'].sum(),
  19. '近半年消费金额':grp_user_h.groupby('用户ID')['实付'].sum(),
  20. '近1年消费金额':grp_user_y.groupby('用户ID')['实付'].sum()}).fillna(0)
  21. # 活跃人群:统计近Q有消费的人群
  22. df_user_rf[df_user_rf['近Q消费次数']>0]
  23. # 流失预警:统计近半年有消费,近一个Q没消费的人群
  24. df_user_rf[(df_user_rf['近半年消费次数']>0) & (df_user_rf['近Q消费次数']<1)]
  25. # 流失用户:统计近1年都没有消费的人群
  26. df_user_rf[df_user_rf['近1年消费次数']<1]
  27. # 年消费额中位数之上人群
  28. median = df_user_m['近1年消费金额'].median()
  29. df_user_m[df_user_m['近1年消费金额']>median].sort_values(by='近1年消费金额',ascending=False)
  30. # 查看消费最高用户在2020年的消费记录
  31. df_shop['2020'][df_shop['用户ID']=='U00000059']

根据RFM模型,我们可以把用户划分成多个等级,可以借助cut方法对区间段划分。

  1. # 汇总用户表
  2. df_user=pd.concat([df_user_rf,df_user_m], axis=1)
  3. # 假设按近1年消费额定义4个用户等级
  4. # (0, 2000], (2000, 5000], (5000, 10000], (10000, ~]
  5. bins = [0, 2000, 5000, 10000, df_user_m['近1年消费金额'].max()]
  6. user_cut = pd.cut(df_user_m['近1年消费金额'], bins,labels=['钻石','黄金','白银','青铜'])
  7. user_cut.value_counts()

根据返回结果,还可以继续调整区间段,辅助等级定义。

产品服务分析

一般交易系统都会把订单和订单明细单独存放,好在pandas支持重复索引,当分别加载完2张表后,可以用merge方法按订单号合并后分析。

  1. # 单店分析
  2. df_shop = pd.read_excel(path.joinpath('sample_SP0000.xlsx'))
  3. df_shopx = pd.read_excel(path.joinpath('sample_SP0000_X.xlsx'))
  4. # 合并订单表和订单明细表
  5. df_shopa = df_shop.reset_index().merge(df_shopx, on='订单ID', suffixes=['_总订单','_单项产品']).set_index('订单日期')
  6. # 单店分析产品,统计各产品销售数量
  7. # 历年产品销量统计
  8. df_prod = pd.DataFrame({'总销量':df_shopa.groupby('产品')['数量'].sum().sort_values(ascending=False)})
  9. df_prod['总销量排名'] = df_prod.rank(ascending=False)
  10. # 统计各年产品销量榜
  11. df_shopa.reset_index().groupby([pd.Grouper(key='订单日期',freq='A'),
  12. pd.Grouper(key='产品')])['数量'].sum().unstack('产品').to_period('A')
  13. # 统计2019年最畅销产品
  14. df_shopa['2019'].groupby('产品')['数量'].sum().sort_values(ascending=False)
  15. # 全国范围分析
  16. df_shopsa = df_shops.reset_index().merge(df_shops_x, on='订单ID', suffixes=['_总订单','_单项产品']).set_index('订单日期')
  17. df_prods = pd.DataFrame({'总销量':df_shopsa.groupby('产品')['数量'].sum().sort_values(ascending=False)})
  18. df_prods['总销量排名'] = df_prods.rank(ascending=False)
  19. df_prods[df_prods['总销量排名']<=10]

以上分析都是围绕门店、用户和产品3大重要维度的常见分析内容。
实战中还会根据实际情况制定更多分析目标,如成本毛利、日增客户、复购率、节日活动销量、推广转化、人效坪效等等。

总结

本文介绍了pandas分析中常用的功能,包括多层索引、结构重塑、合并关联、交叉透视、分组聚合、时间序列。
此外,通过门店型业务案例,演示如何应用分析功能,完成分析目标。
实战中,需要灵活应用各个分析功能,学习的关键是建立起各功能应用效果的直观感受。
下一节会介绍数据可视化,可以借助图表帮助加强数据的直观感受。

扫码加入学习群,前100名免费。

6、数据分析 - 图5