使用 Pandas 进行数据分析的一些自定义函数

623 阅读5分钟

前言

最近经常用到 Pandas 来进行一些数据分析,由于自己对 Pandas 使用的不熟练,也经常一时想不起 Pandans 的某些用法。所以,在这里简单记录下自己平时用到的自定义函数。

这里没有对 Pandas 的介绍,会持续更新的,仅是个人记录而已,不喜勿喷~

使用环境

jupyter notebook

导入所需包

import pandas as pd

1. 列出某列中各个值依次出现的序号

1.1 构建数据

# 构建数据 每次比赛第一名的获得者
data = {'编号': ['01', '02', '01', '01', '02'],
       '姓名': ['小明', '小红', '小明', '小明', '小红'],
       '参赛时间': ['20170804', '20180804', '20190804', '20200804', '20210804']}
df = pd.DataFrame(data)
df

image.png

1.2 需求

求各个第一名获得者分别在不同年份是第几次获得第一名?

1.3 代码

#count_if函数
def countif(line, data, base):
    '''
    line:apply 函数 axis=1 代表当前行数据
    data:需要统计的指定列的数据
    base:df 中需要统计的列索引
    '''    
    value = line[base]          # 获得当前行指定列的值
    end = line['index']+1       # 获得当前行的索引值
    return data.iloc[:end].value_counts()[value]   # 返回指定列中当前行值出现的次数

# reset_index 重置索引
stat_df = df['编号'].reset_index()
df['第几次获得第一名'] = stat_df.apply(countif, axis=1, args=(stat_df['编号'], '编号',))
df

image.png

想着 pandas 会不会有直接满足本需求的函数,找了半天没找到。Excel 中 countif() 可以实现本需求,但是实际数据有15万行,一运行就崩了,不知是我电脑不行,还是 Excel 不熟悉。就想着用 Pandas 来实现,又不想写 for 循环,那样的话感觉完全体现不了 Pandas 的优势(啥优势咱也说不清楚,可能是不像 Pandaser -- 使用 Pandas 的人,哈哈~)。最后想起了 apply 函数,但是其函数只能获得当前行或者当前列的值,无法获得当前行的索引(或许是我不知道吧)。

故此,只能用 reset_index() 重置索引,将常数索引变成一列。 df['编号'].reset_index() 运行的效果:

image.png

然后,在 countif() 函数 中利用当前行的 index 获取指定列的数据片段,通过 value_counts() 来统计频数从而获得当前行指定列的值 value 出现的次数。

注:

  • 关于 countif() 函数中的 data 我也取了好几次值,从 “df” 到 “stat_df”,最后发现还是“stat_df['编号']”最好,一次次缩小了 data 。
  • 其实,我这里好像还是少了一步,应该先对“参赛时间”进行排序的,不过这个不是重点。

用上面的方法去跑15W行的数据,居然跑了近2个小时,我感觉我应该没有做到一个 Pandaser 的做法。然后想着用 groupby() 分下组,然后用 apply() + 自定义函数(内部调用 rank())。写着写着,发现根本就用不着 apply(),GROUPBY 自身就有 rank()。这太好了!只是 GROUPBY.rank() 没有 numeric_only 参数,需要将"参赛时间"转为时间类型,因为时间类型其实底层也是数值,所以能用 GROUPBY.rank()。

df['参赛时间'] = pd.to_datetime(df['参赛时间'])
df['第几次'] = df.groupby('编号')['参赛时间'].rank()
df

image.png

15W行数据轻松搞定!!!

2. 统计频数和频率

2.1 构建数据

沿用 1.1 的数据。

2.2 需求

1) 某人获取第一名的频数和频率。 2) 所有人获取第一名的“频数/总数”和频率。

2.3 代码

# 统计一列的数据分布
def frequency_distribution(data, values=None, is_count=False, dropna=True):
    # data: Series 数据
    # values:list 需要统计的值
    # is_count:bool 是否输出总数,形如 频数/总数
    # dropna: value_counts 本身的参数,空值是否删除
    
    if isinstance(values, list):
        stat_values = values
    elif values is None:
        stat_values = list(data.dropna().unique())
    else:
        stat_values = [values]
        
    # count 统计总数
    if dropna:
        count = data.count()
    else:
        count = len(data)

    frequency = data.value_counts(ascending=True, dropna=dropna) # 频数
    relative_frequency = data.value_counts(normalize=True, dropna=dropna) # 频率
    for value, number in frequency.items():
        # 此处的 key 是 需要统计的值
        if value in stat_values:
            if is_count:
                print('{0}\t{1}/{2}\t{3:4.2f}%'.format(value, number, count, relative_frequency[value] * 100))
            else:
                print('{0}\t{1}\t{2:4.2f}%'.format(value, number, relative_frequency[value] * 100))
            stat_values.remove(value) # 移除已统计的
                
    # 可能统计的值 不在 value_counts 的结果里
    for value in stat_values:
        if value not in frequency.keys():
            if is_count:
                print('{0}\t{1}/{2}\t{3:4.2f}%'.format(value, 0, count, 0))
            else:
                print('{0}\t{1}\t{2:4.2f}%'.format(value, 0, 0))

注:

  • 这里我没有将结果数据封装为 DataFrame,因为 print 就能满足我的要求。
  • 这里百分位的小数是取的两位,若需,请自行修改,或者设置成一个参数。 关于上述两个问题,本人较懒,你可以自行修改。

代码修改记录:

  • 20210804 15:35 新增 dropna 参数

2.4 示例

2.4.1 获取第一名的所有人的频数和频率
frequency_distribution(df['姓名'])

image.png

2.4.2 获取第一名的所有人的频数和频率,并显示总数
frequency_distribution(df['姓名'], is_count=True)

image.png

2.4.3 “小红”获取第一名的频数和频率,并显示总数
frequency_distribution(df['姓名'], '小红', is_count=True)

image.png

2.4.4 所有参赛人员的获取第一名的频数和频率,并显示总数,即使某人没有获得第一名也显示

此时,只需获得所有参赛人员的名单即可。

frequency_distribution(df['姓名'], ['小红','小明','小张'], is_count=True)

image.png

3. 基本统计项

3.1 代码

def mean_std_ci(data, ci=0.95):
    """
    计算 均值 标准差 95%CI 中位数 范围(最小, 最大)
    """
    describe = data.describe()
    mean = describe['mean']
    std = describe['std']
    count = describe['count']
    
    # 95% 置信区间
    tscore = stats.t.ppf(1-(1-ci)/2, count - 1)
    lower = mean - (tscore*std/(count**0.5))
    upper = mean + (tscore*std/(count**0.5))
    
    median = describe['50%']
    min_ = describe['min']
    max_ = describe['max']
    
    Q1 = describe['25%']
    Q3 = describe['75%']
    
    
    print('Count:{0:.0f}'.format(count))
    print('Mean ± SD:{0:.2f} ± {1:.2f}'.format(mean, std))
    print('95% CI:{0:.2f} to {1:.2f}'.format(lower, upper))
    print('Median:{0:.2f}'.format(median))
    print('Range (min, max):{0:.2f}, {1:.2f}'.format(min_, max_))
    print('Q1~Q3:{0:.2f}~{1:.2f}'.format(Q1, Q3))
    print("\n==================================\n")
    
    IQR = Q3-Q1
    print(f'IQR:{IQR:.2f}')
    
    # 异常值
    in_range_1 = Q1 - 1.5*IQR
    in_range_3 = Q3 + 1.5*IQR
    
    out_range_1 = Q1 - 3*IQR
    out_range_3 = Q3 + 3*IQR
    print(f'內限:{out_range_1:.2f}~{in_range_1:.2f}, {in_range_3:.2f}~{out_range_3:.2f}')
    print(f'外限:<{out_range_1:.2f}, >{out_range_3:.2f}')
    
    in_data = data[((data>=out_range_1) & (data<in_range_1)) | ((data>in_range_3) & (data<=out_range_3))]
    out_data = data[(data<out_range_1)  | (data>out_range_3)]
    print(f'温和异常值:{list(in_data)}')
    print(f'极端异常值:{list(out_data)}')