Python---Pandas万字总结(3)

Pandas数据处理模块

在完成数据加载之后,可能需要对事实表和维度表进行连接,这是对数据进行多维度拆解的基础;我们可能从不同的数据源加载了结构相同的数据,之后可能需要将这些数据拼接起来;这些操作被统称为数据重塑。当然,在如今信息化发展迅速,数据量庞大的时代,我们拿到的数据未必是质量很好的,可能还需要对数据中的缺失值、重复值、异常值进行适当的处理。即便我们获取的数据在质量上是没有问题的,但也可能需要对数据进行一系列的预处理,才能满足我们做数据分析的需求。接下来,我们来梳理这方面的知识。

数据重塑

有的时候,我们做数据分析需要的原始数据可能并不是来自一个地方,就像上一文章的例子中,我们从关系型数据库中读取了三张表,得到了三个DataFrame对象,但实际工作可能需要我们把他们的数据整合到一起。例如:emp_dfemp2_df其实都是员工的数据,而且数据结构完全一致,可以使用pandas提供的concat函数实现两个或多个DataFrame的数据拼接,代码如下所示。

all_emp_df = pd.concat([emp_df, emp2_df])

输出:

        ename    job        mgr      sal     comm    dno
eno
1359    胡一刀    销售员	   3344.0	1800	200.0	30
2056    乔峰	    分析师	    7800.0	 5000	 1500.0	 20
3088    李莫愁	   设计师	   2056.0	3500	800.0	20
3211    张无忌	   程序员	   2056.0	3200	NaN     20
3233    丘处机	   程序员	   2056.0	3400	NaN	    20
3244    欧阳锋	   程序员	   3088.0	3200	NaN     20
3251    张翠山	   程序员	   2056.0	4000	NaN	    20
3344    黄蓉	    销售主管   7800.0	3000	800.0	30
3577    杨过	    会计	     5566.0	  2200	  NaN	  10
3588    朱九真	   会计	    5566.0	 2500	 NaN	 10
4466    苗人凤	   销售员	   3344.0	2500	NaN	    30
5234    郭靖	    出纳	     5566.0	  2000	  NaN	  10
5566    宋远桥	   会计师	   7800.0	4000	1000.0	10
7800    张三丰	   总裁	    NaN      9000	 1200.0	 20
9500	张三丰	   总裁	    NaN	     50000	 8000.0	 20
9600	王大锤    程序员	   9800.0	8000	600.0	20
9700	张三丰	   总裁	    NaN	     60000	 6000.0	 20
9800	骆昊	    架构师	    7800.0	 30000	 5000.0	 20
9900	陈小刀	   分析师	   9800.0	10000	1200.0	20

上面的代码将两个代表员工数据的DataFrame拼接到了一起,接下来我们使用merge函数将员工表和部门表的数据合并到一张表中,代码如下所示。

先使用reset_index方法重新设置all_emp_df的索引,这样eno 不再是索引而是一个普通列,reset_index方法的inplace参数设置为True表示,重置索引的操作直接在all_emp_df上执行,而不是返回修改后的新对象。

all_emp_df.reset_index(inplace=True)

通过merge函数合并数据,当然,也可以调用DataFrame对象的merge方法来达到同样的效果。

pd.merge(all_emp_df, dept_df, how='inner', on='dno')

输出:

    eno	    ename	job	     mgr	 sal	 comm	 dno	dname	 dloc
0	1359	胡一刀	 销售员	3344.0	1800	200.0	30	   销售部	 重庆
1	3344	黄蓉	  销售主管	7800.0	3000	800.0	30	   销售部	 重庆
2	4466	苗人凤	 销售员	3344.0	2500	NaN	    30	   销售部	 重庆
3	2056	乔峰	  分析师	 7800.0	 5000	 1500.0	 20	    研发部	  成都
4	3088	李莫愁	 设计师	2056.0	3500	800.0	20	   研发部	 成都
5	3211	张无忌  程序员	2056.0	3200	NaN	    20	   研发部	 成都
6	3233	丘处机	 程序员	2056.0	3400	NaN	    20	   研发部	 成都
7	3244	欧阳锋	 程序员	3088.0	3200	NaN	    20	   研发部	 成都
8	3251	张翠山	 程序员	2056.0	4000	NaN	    20	   研发部	 成都
9	7800	张三丰	 总裁	     NaN	 9000	 1200.0	 20	    研发部	  成都
10	9500	张三丰	 总裁	     NaN	 50000	 8000.0	 20	    研发部	  成都
11	9600	王大锤	 程序员	9800.0	8000	600.0	20	   研发部	 成都
12	9700	张三丰	 总裁	     NaN	 60000	 6000.0	 20	    研发部	  成都
13	9800	骆昊	  架构师	 7800.0	 30000	 5000.0	 20	    研发部	  成都
14	9900	陈小刀	 分析师	9800.0	10000	1200.0	20	   研发部	 成都
15	3577	杨过	  会计	  5566.0  2200	  NaN	  10	会计部	  北京
16	3588	朱九真	 会计	     5566.0	 2500	 NaN	 10	   会计部	 北京
17	5234	郭靖	  出纳	  5566.0  2000	  NaN	  10	会计部	  北京
18	5566	宋远桥	 会计师	7800.0	4000	1000.0	10	  会计部	北京

merge函数的一个参数代表合并的左表、第二个参数代表合并的右表。DataFrame对象的合并跟数据库中的表连接非常类似,所以上面代码中的how代表了合并两张表的方式,有leftrightinnerouter四个选项;而on则代表了基于哪个列实现表的合并,相当于 SQL 表连接中的连表条件,如果左右两表对应的列列名不同,可以用left_onright_on参数取代on参数分别进行指定。

pd.merge(all_emp_df, dept_df, how='right', on='dno')

运行结果比之前的输出多出了如下所示的一行,这是因为how='right'代表右外连接,也就意味着右表dept_df中的数据会被完整的查出来,但是在all_emp_df中又没有编号为40 部门的员工,所以对应的位置都被填入了空值。

19	NaN    NaN    NaN    NaN    NaN     NaN    40    运维部    深圳

数据清洗

通常,从 Excel、CSV 或数据库中获取到的数据并不是非常完美的,里面可能因为系统或人为的原因混入了重复值或异常值,也可能在某些字段上存在缺失值;再者,DataFrame中的数据也可能存在格式不统一、量纲不统一等各种问题。因此,在开始数据分析之前,对数据进行清洗就显得特别重要。

缺失值

可以使用DataFrame对象的isnullisna方法来找出数据表中的缺失值,如下所示。

emp_df.isnull()

或者

emp_df.isna()

输出:

        ename   job	    mgr     sal     comm    dno
eno						
1359	False	False	False	False	False	False
2056	False	False	False	False	False	False
3088	False	False	False	False	False	False
3211	False	False	False	False	True	False
3233	False	False	False	False	True	False
3244	False	False	False	False	True	False
3251	False	False	False	False	True	False
3344	False	False	False	False	False	False
3577	False	False	False	False	True	False
3588	False	False	False	False	True	False
4466	False	False	False	False	True	False
5234	False	False	False	False	True	False
5566	False	False	False	False	False	False
7800	False	False	True	False	False	False

相对应的,notnullnotna方法可以将非空的值标记为True。如果想删除这些缺失值,可以使用DataFrame对象的dropna方法,该方法的axis参数可以指定沿着0轴还是1轴删除,也就是说当遇到空值时,是删除整行还是删除整列,默认是沿0轴进行删除的,代码如下所示。

emp_df.dropna()

输出:

        ename   job      mgr	 sal    comm     dno
eno						
1359	胡一刀  销售员	3344.0	1800   200.0	30
2056	乔峰    架构师	 7800.0	 5000	1500.0	 20
3088	李莫愁  设计师	2056.0	3500   800.0	20
3344	黄蓉    销售主管	7800.0	3000   800.0	30
5566	宋远桥  会计师	7800.0	4000   1000.0	10

如果要沿着1轴进行删除,可以使用下面的代码。

emp_df.dropna(axis=1)

输出:

        ename    job      sal    dno
eno				
1359	胡一刀   销售员    1800	30
2056	乔峰     架构师	  5000	 20
3088	李莫愁   设计师    3500	20
3211	张无忌   程序员    3200	20
3233	丘处机   程序员    3400	20
3244	欧阳锋   程序员    3200	20
3251	张翠山   程序员    4000	20
3344	黄蓉     销售主管  3000	30
3577	杨过     会计	   2200	  10
3588	朱九真   会计	  2500	 10
4466	苗人凤   销售员	 2500   30
5234	郭靖     出纳      2000   10
5566	宋远桥   会计师    4000   10
7800	张三丰   总裁      9000   20

注意DataFrame对象的很多方法都有一个名为inplace的参数,该参数的默认值为False,表示我们的操作不会修改原来的DataFrame对象,而是将处理后的结果通过一个新的DataFrame对象返回。如果将该参数的值设置为True,那么我们的操作就会在原来的DataFrame上面直接修改,方法的返回值为None。简单的说,上面的操作并没有修改emp_df,而是返回了一个新的DataFrame对象。

在某些特定的场景下,我们可以对空值进行填充,方法是fillna,填充空值时可以使用指定的值(通过value参数进行指定),也可以用表格中前一个单元格(通过设置参数method=ffill)或后一个单元格(通过设置参数method=bfill)的值进行填充,当代码如下所示。

emp_df.fillna(value=0)

注意:填充的值如何选择也是一个值得探讨的话题,实际工作中,可能会使用某种统计量(如:均值、众数等)进行填充,或者使用某种插值法(如:随机插值法、拉格朗日插值法等)进行填充,甚至有可能通过回归模型、贝叶斯模型等对缺失数据进行填充。

输出:

        ename    job        mgr      sal     comm    dno
eno
1359	胡一刀    销售员	   3344.0	1800	200.0	30
2056	乔峰	    分析师	    7800.0	 5000	 1500.0	 20
3088	李莫愁	   设计师	   2056.0	3500	800.0	20
3211	张无忌	   程序员	   2056.0	3200	0.0     20
3233	丘处机	   程序员	   2056.0	3400	0.0	    20
3244	欧阳锋	   程序员	   3088.0	3200	0.0     20
3251	张翠山	   程序员	   2056.0	4000	0.0	    20
3344	黄蓉	    销售主管   7800.0	3000	800.0	30
3577	杨过	    会计	     5566.0	  2200	  0.0	  10
3588	朱九真	   会计	    5566.0	 2500	 0.0	 10
4466	苗人凤	   销售员	   3344.0	2500	0.0	    30
5234	郭靖	    出纳	     5566.0	  2000	  0.0	  10
5566	宋远桥	   会计师	   7800.0	4000	1000.0	10
7800	张三丰	   总裁	    0.0      9000	 1200.0	 20
重复值

接下来,我们先给之前的部门表添加两行数据,让部门表中名为“研发部”和“销售部”的部门各有两个。

dept_df.loc[50] = {'dname': '研发部', 'dloc': '上海'}
dept_df.loc[60] = {'dname': '销售部', 'dloc': '长沙'}
dept_df

输出:

    dname  dloc
dno		
10	会计部	北京
20	研发部	成都
30	销售部	重庆
40	运维部	天津
50	研发部	上海
60	销售部	长沙

现在,数据表中有重复数据了,可以通过DataFrame对象的duplicated方法判断是否存在重复值,该方法在不指定参数时默认判断行索引是否重复,也可以指定根据部门名称dname判断部门是否重复,代码如下所示。

dept_df.duplicated('dname')

输出:

dno
10    False
20    False
30    False
40    False
50     True
60     True
dtype: bool

从上面的输出可以看到,5060两个部门从部门名称上来看是重复的,如果要删除重复值,可以使用drop_duplicates方法,该方法的keep参数可以控制在遇到重复值时,保留第一项还是保留最后一项,或者多个重复项一个都不用保留,全部删除掉。

dept_df.drop_duplicates('dname')

输出:

	dname	dloc
dno		
10	会计部	北京
20	研发部	成都
30	销售部	重庆
40	运维部	天津

keep参数的值修改为last

dept_df.drop_duplicates('dname', keep='last')

输出:

	dname	dloc
dno		
10	会计部	北京
40	运维部	天津
50	研发部	上海
60	销售部	长沙

使用同样的方式,我们也可以清除all_emp_df中的重复数据,例如我们认定“ename”和“job”两个字段完全相同的就是重复数据,我们可以用下面的代码去除重复数据。

all_emp_df.drop_duplicates(['ename', 'job'], inplace=True)

说明:上面的drop_duplicates方法添加了参数inplace=True,该方法不会返回新的DataFrame对象,而是在原来的DataFrame对象上直接删除,之后去查看all_emp_df看看是不是已经移除了重复的员工数据。

异常值

异常值在统计学上也称作离群点(outlier),异常值的分析也称作离群点分析。异常值是指样本中出现的“极端值”,数据值看起来异常大或异常小,其分布明显偏离其余的观测值。实际工作中,有些异常值可能是由系统或人为原因造成的,但有些异常值却不是,它们能够重复且稳定的出现,属于正常的极端值,例如很多游戏产品中头部玩家的数据往往都是离群的极端值。所以,我们不能忽视异常值的存在,也不能简单地把异常值从数据分析中剔除。重视异常值的出现,分析其产生的原因,常常成为发现问题进而改进决策的契机。

异常值的检测有Z-score 方法、IQR 方法、DBScan 聚类、孤立森林等,这里仅对前两种方法做一个简单的介绍。

如果数据服从正态分布,依据3σ法则,异常值被定义与平均值的偏差超过三倍标准差的值。在正态分布下,距离平均值3σ之外的值出现的概率为$ P(|x-\mu|>3\sigma)<0.003 $,属于小概率事件。如果数据不服从正态分布,那么可以用远离均值的多少倍的标准差来描述,这里的倍数就是Z-score。Z-score以标准差为单位去度量某一原始分数偏离平均值的距离,公式如下所示。
z = X − μ σ ∣ z ∣ > 3 z = \frac {X - \mu} {\sigma} \\ |z| > 3 z=σXμz>3
Z-score需要根据经验和实际情况来决定,通常把远离标准差3倍距离以上的数据点视为离群点,下面的代给出了如何通过Z-score方法检测异常值。

def detect_outliers_zscore(data, threshold=3):
    avg_value = np.mean(data)
    std_value = np.std(data)
    z_score = np.abs((data - avg_value) / std_value)
    return data[z_score > threshold]

IQR 方法中的IQR(Inter-Quartile Range)代表四分位距离,即上四分位数(Q3)和下四分位数(Q1)的差值。通常情况下,可以认为小于 $ Q1 - 1.5 \times IQR $ 或大于 $ Q3 + 1.5 \times IQR $ 的就是异常值,而这种检测异常值的方法也是箱线图(后面会讲到)默认使用的方法。下面的代码给出了如何通过 IQR 方法检测异常值。

def detect_outliers_iqr(data, whis=1.5):
    q1, q3 = np.quantile(data, [0.25, 0.75])
    iqr = q3 - q1
    lower, upper = q1 - whis * iqr, q3 + whis * iqr
    return data[(data < lower) | (data > upper)]

如果要删除异常值,可以使用DataFrame对象的drop方法,该方法可以根据行索引或列索引删除指定的行或列。例如我们认为月薪低于2000或高于8000的是员工表中的异常值,可以用下面的代码删除对应的记录。

emp_df.drop(emp_df[(emp_df.sal > 8000) | (emp_df.sal < 2000)].index)

如果要替换掉异常值,可以通过给单元格赋值的方式来实现,也可以使用replace方法将指定的值替换掉。例如我们要将月薪为18009000的替换为月薪的平均值,补贴为800的替换为1000,代码如下所示。

avg_sal = np.mean(emp_df.sal).astype(int)
emp_df.replace({'sal': [1800, 9000], 'comm': 800}, {'sal': avg_sal, 'comm': 1000})
预处理

对数据进行预处理也是一个很大的话题,它包含了对数据的拆解、变换、归约、离散化等操作。先来看看数据的拆解。如果数据表中的数据是一个时间日期,我们通常都需要从年、季度、月、日、星期、小时、分钟等维度对其进行拆解,如果时间日期是用字符串表示的,可以先通过pandasto_datetime函数将其处理成时间日期。

在下面的例子中,我们先读取 Excel 文件,获取到一组销售数据,其中第一列就是销售日期,我们将其拆解为“月份”、“季度”和“星期”,代码如下所示。

sales_df = pd.read_excel(
    'data/销售数据.xlsx',
    usecols=['销售日期', '销售区域', '销售渠道', '品牌', '销售额']
)
sales_df.info()

说明:上面代码中使用了相对路径来获取 Excel 文件,也就是说 Excel 文件在当前工作路径下名为data的文件夹中

输出:

<class 'pandas.core.frame.DataFrame'>
RangeIndex: 1945 entries, 0 to 1944
Data columns (total 5 columns):
 #   Column  Non-Null Count  Dtype         
---  ------  --------------  -----         
 0   销售日期    1945 non-null   datetime64[ns]
 1   销售区域    1945 non-null   object        
 2   销售渠道    1945 non-null   object        
 3   品牌        1945 non-null   object        
 4   销售额      1945 non-null   int64         
dtypes: datetime64[ns](1), int64(1), object(3)
memory usage: 76.1+ KB
sales_df['月份'] = sales_df['销售日期'].dt.month
sales_df['季度'] = sales_df['销售日期'].dt.quarter
sales_df['星期'] = sales_df['销售日期'].dt.weekday
sales_df

输出:

	    销售日期	 销售区域	销售渠道	品牌	  销售额	月份	季度	星期
0	    2020-01-01	上海	     拼多多	 八匹马  8217 1	 1	  2
1	    2020-01-01	上海	     抖音	 八匹马	6351 1	  1	  2
2	    2020-01-01	上海	     天猫	 八匹马	14365 1	  1	  2
3	    2020-01-01	上海	     天猫     八匹马	2366  1	  1   2
4	    2020-01-01	上海	     天猫 	  皮皮虾	15189 1	  1   2
5   	2020-12-30	北京	     京东	 花花姑娘 6994 12  4	  2
6  	    2020-12-30	福建	     实体	  八匹马	 7663 12  4	  2
7  	    2020-12-31	福建	     实体	 花花姑娘 14795 12 4   3
8       2020-12-31	福建	     抖音	  八匹马	 3481  12 4   3
9    	2020-12-31	福建	     天猫	  八匹马	 2673 12  4	  3

在上面的代码中,通过日期时间类型的Series对象的dt 属性,获得一个访问日期时间的对象,通过该对象的yearmonthquarterhour等属性,就可以获取到年、月、季度、小时等时间信息,获取到的仍然是一个Series对象,它包含了一组时间信息,所以我们通常也将这个dt属性称为“日期时间向量”。

对于字符串类型数据,可以使用pandasget_dummies()函数来生成哑变量(虚拟变量)矩阵,代码如下所示。

persons_df = pd.DataFrame(
    data={
        '姓名': ['关羽', '张飞', '赵云', '马超', '黄忠'],
        '职业': ['医生', '医生', '程序员', '画家', '教师'],
        '学历': ['研究生', '大专', '研究生', '高中', '本科']
    }
)
persons_df

输出:

	姓名	职业	学历
0	关羽	医生	研究生
1	张飞	医生	大专
2	赵云	程序员 研究生
3	马超	画家	高中
4	黄忠	教师	本科

将职业处理成哑变量矩阵。

pd.get_dummies(persons_df['职业'])

输出:

    医生 教师  画家  程序员
0	1    0    0    0
1	1    0    0    0
2	0    0    0    1
3	0    0    1    0
4	0    1    0    0

将学历处理成大小不同的值。

def handle_education(x):
    edu_dict = {'高中': 1, '大专': 3, '本科': 5, '研究生': 10}
    return edu_dict.get(x, 0)


persons_df['学历'].apply(handle_education)

输出:

0    10
1     3
2    10
3     1
4     5
Name: 学历, dtype: int64
  • 27
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值