Python笔记 - 数据预处理分析基础(上)

代码星辉·七月创作之星挑战赛 10w+人浏览 287人参与

文章是本人基于《Python数据分析与挖掘实战 第2版》的学习笔记与记录,如需更体系化与理论化(公式原理)的内容,可以直接查阅这本书,我是在学习之后将自己的理解写成这篇文章,会添加一些书外的相关内容,内核与主题都和书是一样的,然后内容大致可以对标第三、四章。(书上一些实战的范例都会复现,不过书上的有些代码是跑不通的,所以本文做了一些修改)

一、数据质量分析

1.缺失值

①原因

拿到的数据里面可能会有空白的情况,这些就是缺失值。

数据缺失一般可能是因为本身就不存在这种的值,比方说麦当劳中冰鲜柠檬水的销量,麦当劳的菜单里根本没有冰鲜柠檬水这个选择,自然而然也就没有销量这个指标,也便有了缺失值;

还有一种可能是在数据挖掘或者是爬取的时候,由于技术问题或者意外情况导致的数据缺失,就比方说我在之前一次对招聘网站的信息爬取当中,偶尔会跳出人机识别,需要我去点那个验证的方框,而我不知道怎么解决,所以在爬取信息的时候可能就会因为这个人机识别打断过程,令得本来正常爬取应该得到的信息缺失了。

②处理

I 缺太多

如果一个指标对应的数据信息缺失太多的话,一般我们就直接把它删掉。一般删除的阈值是40%,也就是你这个指标里面的数据缺超过四成,而且还没有什么力气和手段把它补全了,那咱就不要它了。

依旧是拿我之前爬取招聘网站来举例,我们爬到最后2025年、剩下三个城市的招聘信息,却突然被网站的反爬机制的阻挡在外了。对于这三个爬取进度都没有超过40%的城市数据,我们就直接删除了对应的指标。

II 缺部分

如果确实是缺数据了,但是缺的体量相对占比还比较低,那我们就可以考虑用插值补值来让这个指标对整个分析流程保持效力。

这部分书上一笔带过了,少了几个方法。然后针对拉格朗日插值法,我就跟着书过了一遍实战。

i 均值/中位数插值

一般对于能够定量的数据,也就是能够用具体的数值处理的数据,比方说这个群体的年龄、身高,我们就可以考虑用均值或者是中位数插补。

那么这两个在定量数据的背景下怎么选择呢?

针对整体呈正态分布的数据,我们采用均值插补,因为这种数据的分布就是比较稳定,均值也不会受到非常极端的极差影响。

而针对偏态分布的数据,那就得用中位数插补了,因为这种极值对均值的影响是很大的,而相比起来,中位数可以当成一个定值,面对那些离群值和极端值特殊的情况也会更加稳健。

ii 众数插值

对于能够定性的数据,也就是能够划定成一个分支亦或是分类的,比方说性别、满意度还有0-1决策变量(是/不是),我们就可以考虑用众数插补

ad:前两种尽量在小比例缺失的时候选择,不然补的占大头会很影响分析。然后总的来说,这些就适用于人口、经济统计类的数据,对标数学建模大部分比赛的C题(经管类)

iii 定值插值

如果我们在查资料或者是文献的时候查到了,这个数据有宏观对应的统一标准,就可以用已知的标准作为定值,反正有文献或者资料支持,偏差也不会很大

iv 邻值插值

对于和相邻的数据点有逻辑联系的缺失部分,比方说一串时间序列的数据,我们就可以用邻值来对缺失值插补

v 牛顿插值法

对于需要修改或者动态增加节点的数据,我们可以用牛顿插值,这个比拉格朗日插值法的效率会高一些,整体可以用递推来理解。

ad:牛顿插值法对需要求导数的不适用,而且适用之后的区间边缘也会有比较大的波动,一般在热力、地理遥感这个部分适用,大概对标的是数学建模大部分比赛的B题(跨学科)

vi 样条插值法

对于曲线、曲率都是连续也就是呈分段光滑的曲线的数据,可以考虑样条插值,它对精度要求的适配非常的高

vii 拉格朗日插值法 

点数不多且固定、每个点对应的函数值已知的数据,可以用拉格朗日插值。

首先我们先拿来书中的数据包:

时间

(日)

131415161819202122232425
销售额(元)

3036.8

空值

2699.3

2332.1

3295.5

3614.7

4060.3

6607.4

3744.1

3136.6

3393.1

3442.1

由于我们这里是直接横排全部时间,而且对应的时间是字符串的形式,即2025/2/13... , 所以对代码作出了一定的修改:(其实是书上的代码本来就有问题,很多细节没处理,导致最后跑不通)

import pandas as pd
import numpy as np
from scipy.interpolate import lagrange

file_path = '路径.xlsx'
data = pd.read_excel(file_path)

sales_data = data.iloc[0, 1:]  # 跳过第一列(销售额(元))
dates = data.columns[1:]  # 获取所有日期列名

sales = pd.Series(sales_data.values, index=dates)

for i in range(len(sales)):
    if sales.iloc[i] == "空值":
        sales.iloc[i] = np.nan
    else:
        sales.iloc[i] = float(sales.iloc[i])

#异常值处理
for i in range(len(sales)):
    if pd.notnull(sales.iloc[i]) and (sales.iloc[i] < 1000 or sales.iloc[i] > 5000):
        print(f"异常值 {sales.index[i]} 是 {sales.iloc[i]} ")
        sales.iloc[i] = np.nan

#拉格朗日插值函数定义
def lagrange_interpolate(s, k=2):
    s = s.copy()
    # 获取所有非NaN值的索引位置
    non_nan_indices = np.where(~pd.isna(s))[0]
    # 获取所有NaN值的索引位置
    nan_indices = np.where(pd.isna(s))[0]

    for nan_idx in nan_indices:# 查找左右两侧最近的k个非NaN点
        left_indices = non_nan_indices[non_nan_indices < nan_idx][-k:] if len(
            non_nan_indices[non_nan_indices < nan_idx]) >= k else non_nan_indices[non_nan_indices < nan_idx]
        right_indices = non_nan_indices[non_nan_indices > nan_idx][:k] if len(
            non_nan_indices[non_nan_indices > nan_idx]) >= k else non_nan_indices[non_nan_indices > nan_idx]
        # 合并左右两侧的点
        used_indices = np.concatenate([left_indices, right_indices])

        if len(used_indices) >= 2:  # 至少需要2个点才能插值
            # 获取这些点的x和y值
            x = used_indices
            y = s.iloc[used_indices].values
            # 拉格朗日插值
            try:
                interp_value = lagrange(x, y)(nan_idx)
                s.iloc[nan_idx] = interp_value
                print(f"插值: 索引 {nan_idx} 对应 {s.index[nan_idx]} 插值结果为 {interp_value:.2f}")
            except Exception as e:
                print(f"插值失败: {e}")
    return s

interpolated_sales = lagrange_interpolate(sales)
print("\n插值后是")
print(interpolated_sales)

# 4. 将处理后的数据放回原DataFrame
result = data.copy()
result.iloc[0, 1:] = interpolated_sales.values

 比较需要注意的几点:

一开始要忽略表头,不然会导致后面部分的excel数据没法读入;然后要把“空值”这个字符串改成能操作的拉格朗日函数Nah(这个书上的没有处理,要么就是代码错,要么就是给的数据错,反正这两个肯定错了一个);再接着就是和书里一样的异常值处理,将[1000,5000]作为置信区间,置信区间以外的,也就是2025/2/21的数据会被当成异常值清除,转换成拉格朗日函数Nah(异常值这个后面细讲)。

然后对于拉格朗日插值的def部分,首先建副本后定位到需要插值的Nah,保证它们的周围的都有两个有效的数据点,接着调用拉格朗日函数 scipy.interpolate.lagrange对插值的点求多项式的值,然后插补回去。

结果如下:

二、异常值

①原因

正如其名,异常值就是不合理的数据。要么是录入的时候出现了问题,要么就是信息源有问题,它和缺失值一样也是需要处理的数据。一般是先处理异常值,再处理缺失值。

②判断

I 3\sigma原则

这个一般适用于呈正态分布的数据(总体符合也行),因为它对极端的数据肥肠敏感,一旦异常值过于极端,把均值和标准差拉一下就会使得阈值偏移。个人觉得这个应该比较泛用于A题(物理类)

判断思路是将( \mu -3\sigma , \mu +3\sigma ) 作为置信区间,然后置信区间外面的数据全部当成异常值就行了,整体置信区间覆盖有99.73%,当成噪音数据清除的比例不算很大,

II 箱形图

这个就比较泛用一点,如果没法判断数据的呈现形式可以直接用这个,它的鲁棒性(可以理解说是对异常值的免疫程度)相对会好很多。不过,比起3\sigma原则,箱形图的分析要面对有一定体量的数据,如果数据量太少了可能1会不准。个人感觉泛用在C题(经管类)

判断思路是将置信区间设为(  $ Q_1 - 1.5 \times IQR, \quad Q_3 + 1.5 \times IQR$ ),其中 $Q_1$ 是下四分位数(25%), $Q_3$ 是上四分位数(75%),$IQR = Q_3 - Q_1$ 是四分位距(下标直接从1跳到3是因为$Q_2$ 是中位数,即50%)。需要注意的是在画箱形图之前,要先把全部数据进行一次排序。和3\sigma原则一样,在知心区域外面的全部当成异常值处理。

③处理

这里列四种方法,按照情况选用:

I 删除异常值

这种就是简单直接,但是当样本量较少时直接这么删就会容易导致样本容量不足,稍微大点的还可能改变变量的原有分布,进而影响到我们后续分析结果的准确性。所以我们一般很少用这种方法,除非异常值确实是错误数据,没有任何价值,或者是样本量充足,比方说我之前爬取的招聘数据,一共38w条,即使删除少部分数据也不会影响结论。

II 视为缺失值进行处理

这种和第一种比起来,稍微保守一点,就是将异常值当成了缺失值,利用缺失值处理的那些方法进行填补。这种一般是在异常值不确定对不对,但数据又不太够用了,得尽可能保持数据完备性的时候用的。

III 平均值处理

这种是用异常值前后两个正常观测值的均值来替换该异常值。可以平滑异常的震荡。当然前提是异常值的前后数据变化都比较平稳(比如说平稳的时间序列),对大幅度波动的数据就不太适配。

IV 不做处理

这种比较少见,是把异常值当成是真实有效的数据,由此当成重要的信息保留。比方说我们已经确定异常值是重要信号了(极端事件、故障报警)

二、数据特征分析

1.分布分析

①定量数据

先用极大值减去极小值求极差,然后按照极差决定组距,用极差除组距得到组数,用组数对数据分点,然后得到频率分布表,画出直方图。这个很多步骤在高中就学过了

②定性数据

这种直接就用定性得到的分类分组,画饼图或者是条形图

ad:前面的直方图和条形图虽然看起来都输柱状图的样子,但是这两个不是同一个东西。直方图横轴对应的数据是连续的,条形图的则是离散的。

2.对比分析

这部分我目前觉得常用的就是动态相对数的比较,可以理解成单个或者多个主体的多个时间序列的比较。

首先我们先模拟搓一个书里图像对应的数据包(书上没给,但是直接瞪眼也能大致得到):

①多主体比较

月份A部门B部门C部门
15.58.16.5
26.07.76.3
36.57.86.6
46.18.06.2
56.37.56.4
67.27.76.1
77.88.26.0
87.67.86.2
97.37.26.3
106.97.46.5
116.47.36.6
126.07.06.4

由于书中的代码还是跑不通(版本的字体和符号的问题),所以我将代码做了一些变动:

import pandas as pd
import matplotlib.pyplot as plt
import matplotlib as mpl

plt.rcParams['font.sans-serif'] = ['SimHei'] # 微软雅黑
plt.rcParams['axes.unicode_minus'] = False   # 负号问题

try:
    df_dept_sales = pd.read_excel("路径.xlsx")

    plt.figure(figsize=(10, 6))
    plt.plot(df_dept_sales['月份'], df_dept_sales['A部门'], marker='o', label='A部门', color='green')
    plt.plot(df_dept_sales['月份'], df_dept_sales['B部门'], marker='s', label='B部门', color='red')
    plt.plot(df_dept_sales['月份'], df_dept_sales['C部门'], marker='x', label='C部门', color='skyblue')

    plt.xlabel('月份', fontsize=12)
    plt.ylabel('销售额 (万元)', fontsize=12)
    plt.title('3部门之间销售额的比较', fontsize=14)
    plt.xticks(df_dept_sales['月份'])
    plt.legend(fontsize=10)
    plt.grid(True, linestyle='--', alpha=0.7)
    plt.show()

except Exception as e:
    print(f"处理时发生错误: {e}")

结果和书上的大差不差:

 

②单主体比较

依旧是照着书上可视化图手搓出数据:

月份2012年2013年2014年
18.06.57.3
28.16.07.4
37.95.87.1
47.65.76.9
57.55.67.0
67.65.87.2
78.05.97.5
88.25.87.3
97.85.77.0
107.65.97.1
117.45.77.0
127.35.56.8

然后是稍微修改后的代码(加了横坐标的刻度,还用了try except作异常处理):

import pandas as pd
import matplotlib.pyplot as plt
import matplotlib as mpl

plt.rcParams['font.sans-serif'] = ['SimHei'] # 微软雅黑
plt.rcParams['axes.unicode_minus'] = False   # 负号问题

try:
    df_b_yearly_sales = pd.read_excel("路径.xlsx")

    plt.figure(figsize=(10, 6))
    plt.plot(df_b_yearly_sales['月份'], df_b_yearly_sales['2012年'], marker='o', label='2012年', color='green')
    plt.plot(df_b_yearly_sales['月份'], df_b_yearly_sales['2013年'], marker='s', label='2013年', color='red')
    plt.plot(df_b_yearly_sales['月份'], df_b_yearly_sales['2014年'], marker='x', label='2014年', color='skyblue')

    plt.xlabel('月份', fontsize=12)
    plt.ylabel('销售额 (万元)', fontsize=12)
    plt.title('B部门各年份销售额的比较', fontsize=14)
    plt.xticks(df_b_yearly_sales['月份'])
    plt.legend(fontsize=10)
    plt.grid(True, linestyle='--', alpha=0.7)
    plt.show()

except Exception as e:
    print(f"处理时发生错误: {e}")

结果也是和书上的差不多:

 3.统计量分析

首先先搓出来一个用于模拟的数据:

日期销量
2023/1/1450
2023/1/2500
2023/1/3800
2023/1/41000
2023/1/51200
2023/1/64300
2023/1/74700
2023/1/85200
2023/1/9300
2023/1/104000
2023/1/114900
2023/1/12200

然后我们就可以尝试用desribe分析得到很多数据相关的信息:

import pandas as pd

catering_sale = '路径.xlsx'

data = pd.read_excel(catering_sale, index_col='日期')

data = data[(data['销量'] > 400) & (data['销量'] < 5000)]

statistics = data.describe(percentiles = [0.2, 0.25, 0.4, 0.5, 0.6, 0.75, 0.8])

statistics.loc['极差'] = statistics.loc['max'] - statistics.loc['min']

statistics.loc['变异系数'] = statistics.loc['std'] / statistics.loc['mean']

statistics.loc['四分位数间距'] = statistics.loc['75%'] - statistics.loc['25%']

print(statistics)

 前两行就不用多说了。第三行是指定读取的Excel中'日期'的那一列作为索引,免得又读到表头去了,第四行则是异常值处理,这里和书中一样,把(400, 5000)当成置信区间,然后第五行就是对describe的使用。

我们对一个DataFrame或者Series调用 .describe() 的时候,它就会返回一个摘要的统计表,默认计算一些数据相关的信息

  • count:有效值数量,也就是非空值个数
  • mean:均值
  • std:标准差
  • min:最小值
  • 25%:第25百分位数(上四分位数,Q1)
  • 50%:第50百分位数(中位数,Q2)
  • 75%:第75百分位数(下四分位数,Q3)
  • max:最大值

当然如果我们加上的percentiles=[](也就是像我给的代码那样),它就会强制计算[]中的分位数,而不是默认的25%,50%,75%

要注意一点,我们在覆盖precentiles的时候如果不指定计算上下四分位数的时候,它后续在计算四分位间距的时候就会因为找不到对应算好的数而报错。

第六行和第八行就是直接代入公式了,没有什么需要多讲的。而第七行的变异系数,它是用标准差(std)除均值(mean)得到的,它是用来表示数据变化的幅度有多大,占平均值的比例是多少的比率值。

运行之后,我们就可以得到结果:

4.周期性分析

依旧是照着书上的可视化图搓了一组数据:

日期电量
2012/2/16200
2012/2/26800
2012/2/36500
2012/2/44200
2012/2/54000
2012/2/64300
2012/2/76400
2012/2/86600
2012/2/97000
2012/2/106700
2012/2/114100
2012/2/124200
2012/2/134300
2012/2/146500
2012/2/156700
2012/2/166800
2012/2/176900
2012/2/184300
2012/2/194100
2012/2/204200
2012/2/216600
2012/2/226700
2012/2/237000
2012/2/246900
2012/2/254300
2012/2/264000
2012/2/274100
2012/2/286700
2012/2/296800
2012/3/16900
2012/3/27000
2012/3/34300
2012/3/44000
2012/3/54100
2012/3/66800
2012/3/77000
2012/3/87100
2012/3/96900
2012/3/104200
2012/3/114400
2012/3/124500
2012/3/136500
2012/3/146700
2012/3/156800
2012/3/166900
2012/3/174300
2012/3/184100
2012/3/194300
2012/3/207000
2012/3/216900
2012/3/226700
2012/3/236800
2012/3/244300
2012/3/254200
2012/3/264400
2012/3/276600
2012/3/286800
2012/3/296700
2012/3/306900
2012/3/316200

代码如下:

import pandas as pd
import matplotlib.pyplot as plt

df_normal = pd.read_excel('路径.xlsx')

plt.figure(figsize=(8,4))
plt.plot(df_normal["日期"], df_normal["电量"], marker='', linestyle='-')

plt.xlabel("日期")
plt.ylabel("每日电量")
plt.title("正常用户电量趋势")

x_major_locator = plt.MultipleLocator(7)
ax = plt.gca()
ax.xaxis.set_major_locator(x_major_locator)
plt.xticks(rotation=45)  

plt.rcParams['font.sans-serif'] = ['SimHei']  
plt.rcParams['axes.unicode_minus'] = False   

plt.tight_layout()
plt.show()

由于是Excel,所以我就把csv的路径引用改了,其实没啥区别。接着就是指定了微软雅黑字体,特别解决了符号的问题,中间的X轴时间标签稍作倾斜,美观一些。

复现结果差别不大:
 

5.贡献度分析 

用了一个让人耳熟能详的二八定律,简而言之就是找出贡献值相对更多的那部分指标:

这里书中给了数据了,所以我们只需要把它一字排开复制到Excel中

菜品ID17148171541091171715114286839788426
菜品名A1A2A3A4A5A6A7A8A9A10
盈利/元9173572948113594319530262378197018771782

然后是代码:

import pandas as pd
import matplotlib.pyplot as plt

dish_profit = '路径.xlsx'

df = pd.read_excel(dish_profit, header=None)

print(df)

data_dict = {
    '菜品名': df.iloc[1, 1:].values,  # 第二行第一列开始是菜品名
    '盈利': df.iloc[2, 1:].values    # 第三行第一列开始是盈利
}

data = pd.DataFrame(data_dict)
data = data.set_index('菜品名')
data = data['盈利'].copy()
data = data.sort_values(ascending=False)

plt.rcParams['font.sans-serif'] = ['SimHei']  
plt.rcParams['axes.unicode_minus'] = False

plt.figure(figsize=(10,6))
data.plot(kind='bar')
plt.ylabel('盈利(元)')

p = 1.0 * data.cumsum() / data.sum()
p.plot(color='r', secondary_y=True, style='o-', linewidth=2)

plt.annotate(format(p.iloc[6], '.4%'),
             xy=(6, p.iloc[6]),
             xytext=(6*0.9, p.iloc[6]*0.9),
             arrowprops=dict(arrowstyle="->", connectionstyle="arc3,rad=.2")) # 注释小箭头

plt.ylabel('盈利比例')
plt.title('盈利数据帕累托图')
plt.show()

书上的sort_values只是调用了,但是没有排序,所以我就没有跑通。

然后另外一点,书上的数据输入可能是列读入的,所以直接套用运行在我们复制处理横向展开的数据上就会出问题。所以,我们需要进行一下转置,我们的数据是3行多列,第一行是ID,第二行是菜品名,第三行是盈利,然后构造一个DataFrame,每列做透视成行,用第二行为索引,第三行为盈利值,完成转置。

其他的部分只是稍微作了图像的美化,整体不变,得到的复现结果和书中的也是差不多:

6.相关性分析 

这里有个完全线性相关和线性相关的区别,这里可以用严格单调和单调来理解,完全线性相关就是可以直接用一个函数来映射所有对应的点,而线性相关则是存在一定数量的噪音数据(从图像上看就是有个别点没有完全在画出来的函数线上面),但是总体数据点的排列还是呈一条直线的。

书上给了三种分析方法,这里我复现一下判定系数这部分(其实书中用的是相关系数):

简而言之,判定系数就是当前的回归模型能解释数据变化的程度,越大表示模型越靠谱,然后它的值是相关系数的平方,记作 $r^2$,值域是[0,1]。 当 $r^2$ 接近1意味着二者之间的线性关系很强。当 $r^2$ 接近0明x和y之间几乎没有线性关系,回归方程解释力很拉胯。

复现之前,我们先把书中给出的数据摘出来:

日期百合酱蒸凤爪翡翠蒸香菌饺金银蒸汁蒸排骨乐膳真味鸡蜜汁焗餐包生炒菜心铁板酸菜豆腐香煎韭菜饺香煎萝卜糕原汁原味菜心
2015/1/1176824131318101027
2015/1/21115141391019131413
2015/1/3108121387111099
2015/1/496631097131413
2015/1/5410138121017111314
2015/1/61310131689121159

然后用代码直接对数据进行分析,这里我和书上一样,拿第一道菜也就是百合酱蒸凤爪来当成 y

import pandas as pd

catering_sale = '路径.xlsx'
data = pd.read_excel(catering_sale, index_col='日期')

print(data.corr())

print(data.corr()['百合酱蒸凤爪'])

print(data['百合酱蒸凤爪'].corr(data['翡翠蒸香茜饺']))

难得书上的代码能跑通一次,第二三行是忽略表头情况下的数据读取,第四行是列出相关系数矩阵并计算判定系数,第五行则是只列出我们选定这道菜的相关系数,最后一行则是单独计算指定两道菜的相关系数的。运行后我们就可以得到结果:

首先菜品自己与自己相关系数为1,符合预期,也可以说明这个代码最起码是没有问题的。其他的根据相关系数也可见一斑。

 以上就是本人这一阶段的数据预处理学习笔记,这个模块还有大概一半没学完。

上半部分就先告一段落,感谢阅读。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值