数据组合和缺失值处理
前言
- 前面我们学习的都是只处理加载出来的一份数据,当我们需要处理的数据来自多个文件的时候,我们需要将这几份数据连接到一起,在对其进行操作,那么我们就需要学习数据组合的知识,并学会对缺失值进行处理。
- 以下操作均在 jupyter notebook 中运行
一、数据组合
- 数据组合是指将多个数据元素或数据集通过某种方式(如连接、合并、分组等)组合成一个新的数据集或数据元素的过程。这个过程有助于揭示数据之间的内在联系和潜在规律,为后续的数据分析和决策提供支持。
1.1 concat连接
1.1.1 df对象与 df对象拼接
行拼接参考: 列名, 列拼接参考: 行号
加载并查看数据
import pandas as pd
# 1. 加载数据
df1 = pd.read_csv('data/concat_1.csv')
df2 = pd.read_csv('data/concat_2.csv')
df3 = pd.read_csv('data/concat_3.csv')
# 2. 查看数据
df1
df2
df3
运行结果
- 通过concat()函数, 拼接上述的三个df对象
- 默认是行拼接
# 3. 通过concat()函数, 拼接上述的3个df对象.
row_concat = pd.concat([df1, df2, df3]) # 默认是纵向拼接(行拼接), 结果为: 12行4列
row_concat = pd.concat([df1, df2, df3], axis='rows') # 效果同上
row_concat = pd.concat([df1, df2, df3], axis=0) # 效果同上, 0: 表示行, 1表示列.
row_concat
运行结果
- 按列拼接
row_concat = pd.concat([df1, df2, df3], axis='columns') # 按列拼接, 4行12列
row_concat = pd.concat([df1, df2, df3], axis=1) # 效果同上, 0: 表示行, 1表示列.
row_concat
运行结果
- 行拼接,重置索引
# 通过设置 ignore_case参数, 可以实现: 重置索引
# 行拼接, 重置索引, 结果为: 行索引变为 0 ~ n
pd.concat([df1, df2, df3], ignore_index=True)
运行结果
- 列拼接,重置索引
# 列拼接, 重置索引, 结果为: 列名变为 0 ~ n
pd.concat([df1, df2, df3], axis='columns', ignore_index=True) #
运行结果
1.1.2 df对象与 Serise 对象拼接
代码如下(示例):
# 4. 使用concat连接 df 和 Series
new_series = pd.Series(['n1', 'n2', 'n3', 'n4'])
# 由于Series是列数据(没有行索引), concat()默认是添加行, 所以 它们拼接会新增一列. 缺值用NaN填充
pd.concat([df1, new_series], axis='rows') # 按行拼接
pd.concat([df1, new_series], axis=0) # 0: 行, 1: 列
运行结果
pd.concat([df1, new_series], axis='columns') # 0: 行, 1: 列
pd.concat([df1, new_series], axis=1) # 0: 行, 1: 列
运行结果
1.2 添加新的行跟列
1.2.1 添加行
- 如果想将[‘n1’,‘n2’,‘n3’,‘n4’]作为行连接到df1后,可以创建DataFrame并指定列名.
代码如下(示例):
# 传入二维数组, 即: 1行4列数据.
df5 = pd.DataFrame([['n1', 'n2', 'n3', 'n4']], columns=['A', 'B', 'C', 'D'])
pd.concat([df1, df5])
运行结果
1.2.2 添加列
方式1: 通过 df [列名] = [列值1, 列值2…] 的方式, 可以给 df 添加列
df1['new_col'] = ['n1', 'n2', 'n3', 'n4'] # 正确
# df1['new_col'] = ['n1', 'n2', 'n3', 'n4', 'n5'] # 报错, 值的个数 和 行数(4行)不匹配
# df1['new_col'] = ['n1', 'n2', 'n3'] # 报错, 值的个数 和 行数(4行)不匹配
df1
运行结果
方式2: 通过 df [列名] = Series对象 的方式, 添加1列. # 值的个数和列的个数, 匹不匹配均可
df1['new_col2'] = pd.Series(['n1', 'n2', 'n3'])
df1['new_col2'] = pd.Series(['n1', 'n2', 'n3', 'n4'])
df1['new_col2'] = pd.Series(['n1', 'n2', 'n3', 'n4', 'n5'])
df1
运行结果
1.3 merge方式
加载数据:
# 导包
import sqlite3
# 1. 创建连接对象, 关联: *.db文件.
conn = sqlite3.connect('data/chinook.db')
# 2. 从文件中, 加载 歌曲分类表的信息.
genres_df = pd.read_sql_query('select * from genres;', conn)
genres_df
执行结果
1.3.1 merge()合并数据, 一对一关系
1、加载数据:
# 1. 查看 歌曲表的信息, 并从中找到 不同的音乐风格的数据.
tracks_subset_df = tracks_df.loc[[0, 62, 76, 98, 110, 193, 204, 281]]
tracks_subset_df
执行结果
2、展示其中的三列
tracks_subset_df[['TrackId', 'GenreId', 'Milliseconds']] # 歌曲id, 风格id, 歌曲时长(毫秒)
执行结果
3、合并上述两个表,以风格为标准进行合并:
场景1: 内连接
# 场景1: 内连接
# 参1: 要被合并的df对象.
# 参2: on表示两个df合并的 关联字段, 如果一样可以直接写 on, 如果不一样, 则要写 left_on='左表字段名', right_on='右表字段名'
# 参3: how表示合并方式, 内连接: inner, 左连接: left, 右连接: right, 全(满)连接: outer
genres_df.merge(tracks_subset_df[['TrackId', 'GenreId', 'Milliseconds']], on='GenreId', how='inner')
执行结果
场景2: 左外连接
genres_df.merge(tracks_subset_df[['TrackId', 'GenreId', 'Milliseconds']], on='GenreId', how='left')
执行结果
场景3: 右外连接
genres_df.merge(tracks_subset_df[['TrackId', 'GenreId', 'Milliseconds']], on='GenreId', how='right')
执行结果
场景4: 满外连接, 也叫: 全连接, 即: 它的查询结果 = 左连接 + 右连接, 即: 左表全集 + 右表全集 + 交集.
genres_df.merge(tracks_subset_df[['TrackId', 'GenreId', 'Milliseconds']], on='GenreId', how='outer')
执行结果
场景5: 查看默认是哪种连接.
genres_df.merge(tracks_subset_df[['TrackId', 'GenreId', 'Milliseconds']], on='GenreId') # 默认是: 内连接 => inner
场景6: 如果关联的多个df有重名的列, 则默认会加上 _x, _y这样的后缀, 来源于: suffixes字段.
genres_df.merge(tracks_subset_df[['TrackId', 'Name', 'GenreId', 'Milliseconds']], on='GenreId') # 默认后缀: _x, _y
genres_df.merge(tracks_subset_df[['TrackId', 'Name', 'GenreId', 'Milliseconds']], on='GenreId', suffixes=('_left', '_right')) # 默认后缀: _x, _y
执行结果
1.3.2 merge()合并数据, 多对一关系
需求: 计算每种类型音乐的 平均时长.
# 1. 合并 genres(风格表) 和 tracks(歌曲表)
genres_df.merge(tracks_df[['TrackId', 'Name', 'GenreId', 'Milliseconds']], on='GenreId')
# 需求2: 计算每种类型音乐的平均时长.
# 1. 合并 genres(风格表) 和 tracks(歌曲表). 交集.
genre_track = genres_df.merge(tracks_df[['TrackId', 'GenreId', 'Milliseconds']], on='GenreId', how='inner') # 风格表.merge(歌曲表['歌曲id', '风格id', '歌曲时长毫秒'])
# 左外连接.
genre_track = genres_df.merge(tracks_df[['TrackId', 'GenreId', 'Milliseconds']], on='GenreId', how='left') # 风格表.merge(歌曲表['歌曲id', '风格id', '歌曲时长毫秒'])
genre_track
执行结果
# 2. 根据 风格id分组, 计算 时长的平均值.
genre_time = genre_track.groupby(['GenreId', 'Name']).Milliseconds.mean()
genre_time
执行结果
1.4 join方式
代码如下(示例):
# 1. 加载数据, 获取df对象.
stock_2016 = pd.read_csv('data/stocks_2016.csv')
stock_2017 = pd.read_csv('data/stocks_2017.csv')
stock_2018 = pd.read_csv('data/stocks_2018.csv')
stock_2016
stock_2017
stock_2018
stock_2018的数据
# 2. 默认情况下, join会参考 两个df的 索引列 进行合并连接.
stock_2016.join(stock_2017, lsuffix='_2016', rsuffix='_2017') # 默认: 左外连接
stock_2016.join(stock_2017, lsuffix='_2016', rsuffix='_2017', how='left') # 效果同上
执行结果
# 3. 设置两个df对象的 Symbol列为索引列, 再次关联.
stock_2016.set_index('Symbol')
stock_2017.set_index('Symbol')
# 设置索引列, 并关联.
stock_2016.set_index('Symbol').join(stock_2017.set_index('Symbol'), lsuffix='_2016', rsuffix='_2017', how='left') # 左外连接
stock_2016.set_index('Symbol').join(stock_2017.set_index('Symbol'), lsuffix='_2016', rsuffix='_2017', how='right') # 右外连接
stock_2016.set_index('Symbol').join(stock_2017.set_index('Symbol'), lsuffix='_2016', rsuffix='_2017', how='outer') # 满外连接
stock_2016.set_index('Symbol').join(stock_2017.set_index('Symbol'), lsuffix='_2016', rsuffix='_2017', how='inner') # 内连接
执行结果
# 4. 设置stock_2016的索引为: Symbol 和 stock_2018做关联.
stock_2016
stock_2018.set_index('Symbol')
# 拿着 stock_2016的 指定列(普通列) 和 stock_2018的 索引列 进行关联.
# 细节: on参数设定的是 函数外 df对象的 普通列
stock_2016.join(stock_2018.set_index('Symbol'), lsuffix='_left', rsuffix='_right', on='Symbol', how='outer')
# 总结: join() => 1. 默认是 左外连接. 2.如果两个df有重名字段, 需要手动设置后缀名. 3.默认是根据两个df的 索引列来合并的, 如果想要关联普通列, 需要通过 on 参数实现.
执行结果
二、缺失值简介与处理
- 1、好多数据集都会确实数据,缺失数据有多重表现形式
- 数据库中,缺失数据表示为NULL
- 在某些编程语言中用NA表示
- 缺失值也可能是空字符串(’’)或数值
- 在Pandas中使用NaN表示缺失值
- 2、Pandas中的NaN值来自NumPy库,NumPy中缺失值有几种表示形式:
- NaN,NAN,nan,他们都一样
- 缺失值和其它类型的数据不同,它毫无意义,NaN不等于0,也不等于空串
2.1. 缺失值演示
代码如下(示例):
# 导包
import numpy as np
# 1. 缺失值不是 True, False, 空字符串, 0等, 它"毫无意义"
print(np.NaN == False)
print(np.NaN == True)
print(np.NaN == 0)
print(np.NaN == '')
# 2. np.nan np.NAN np.NaN 都是缺失值, 这个类型比较特殊, 不同通过 == 方式判断, 只能通过API
print(np.NaN == np.nan)
print(np.NaN == np.NAN)
print(np.nan == np.NAN)
# 3. Pandas 提供了 isnull() / isna()方法, 用于测试某个值是否为缺失值
import pandas as pd
print(pd.isnull(np.NaN)) # True
print(pd.isnull(np.nan)) # True
print(pd.isnull(np.NAN)) # True
print(pd.isna(np.NaN)) # True
print(pd.isna(np.nan)) # True
print(pd.isna(np.NAN)) # True
# isnull() / isna()方法 还可以判断数据.
print(pd.isnull(20)) # False
print(pd.isnull('abc')) # False
# 4. Pandas的notnull() / notna() 方法可以用于判断某个值是否为缺失值
print(pd.notnull(np.NaN)) # False
print(pd.notnull('abc')) # True
2.2. 加载缺失值的处理
# 加载数据时可以通过keep_default_na 与 na_values 指定加载数据时的缺失值
pd.read_csv('data/survey_visited.csv')
执行结果
# 加载数据,不包含默认缺失值,
# 参数解释: keep_default_na = False 表示加载数据时, 不加载缺失值.
pd.read_csv('data/survey_visited.csv', keep_default_na=False)
执行结果
# 加载数据,手动指定缺失值, 例如: 指定619, 734为缺失值
# 参数解释: na_values=[值1, 值2...] 表示加载数据时, 设定哪些值为缺失值.
pd.read_csv('data/survey_visited.csv', na_values=['619', '734'], keep_default_na=False)
执行结果
2.3. 删除缺失值
-
dropna()函数, 参数介绍如下:
- subset=None 默认是: 删除有缺失值的行, 可以通过这个参数来指定, 哪些列有缺失值才会被删除
- 例如: subset = [‘Age’] 只有当年龄有缺失才会被删除
- inplace=False 通用参数, 是否修改原始数据默认False
- axis=0 通用参数 按行按列删除 默认行
- how=‘any’ 只要有缺失就会删除 还可以传入’all’ 全部都是缺失值才会被删除
代码如下(示例):
train = pd.read_csv('data/titanic_train.csv')
train.shape # 原始数据, 891行, 12列
# 方式1: 删除缺失值
# 删除缺失值会损失信息,并不推荐删除,当缺失数据占比较低的时候,可以尝试使用删除缺失值
# 按行删除: 删除包含缺失值的记录
# train.dropna().shape # 默认按行删(该行只要有空值, 就删除该行), 结果为: 183行, 12列
train.loc[:10].dropna() # 获取前11行数据, 删除包含空值的行.
# any: 只要有空值就删除该行|列, all: 该行|列 全为空才删除 subset: 参考哪些列的空值. inplace=True 在原表修改
train.dropna(subset=['Age'], how='any')
# 该列值只要有空, 就删除该列值.
train.dropna(how='any', axis=1) # 0(默认): 行, 1: 列
train.isnull().sum() # 快速计算是否包含缺失值
2.4. 填充缺失值
2.4.1 非时序数据填充
代码如下(示例):
# 方式2: 填充缺失值, 填充缺失值是指用一个估算的值来去替代缺失数
# 场景1: 非时间序列数据, 可以使用常量来替换(默认值)
# 用 0 来填充 空值.
train.fillna(0)
# 查看填充后, 每列缺失值 情况.
train.fillna(0).isnull().sum()
# 需求: 用平均年龄, 来替换 年龄列的空值.
train['Age'].fillna(train['Age'].mean())
2.4.2 时序数据填充
代码如下(示例):
# 1. 加载时间序列数据,数据集为印度城市空气质量数据(2015-2020)
# parse_dates: 把某些列转成时间列.
# index_col: 设置指定列为 索引列
city_day = pd.read_csv('data/city_day.csv', parse_dates=['Date'], index_col='Date')
# 2. 查看缺失值情况.
city_day.isnull().sum()
# 3. 数据中有很多缺失值,比如 Xylene(二甲苯)和 PM10 有超过50%的缺失值
# 3.1 查看包含缺失数据的部分
city_day['Xylene'][50:64]
# 3.2 用固定值填充, 例如: 该列的平均值.
# 查看平均值.
city_day['Xylene'].mean() # 3.0701278234985114
# 用平均值来填充.
city_day.fillna(city_day['Xylene'].mean())[50:64]['Xylene']
# 3.3 使用ffill 填充,用时间序列中空值的上一个非空值填充
# NaN值的前一个非空值是0.81,可以看到所有的NaN都被填充为0.81
city_day.fillna(method='ffill')[50:64]['Xylene']
# 3.4 使用bfill填充,用时间序列中空值的下一个非空值填充
# NaN值的后一个非空值是209,可以看到所有的NaN都被填充为209
city_day.fillna(method='bfill')[50:64]['Xylene']
# 3.5 线性插值方法填充缺失值
# 时间序列数据,数据随着时间的变化可能会较大。使用bfill和ffill进行插补并不是解决缺失值问题的最优方案。
# 线性插值法是一种插补缺失值技术,它假定数据点之间存在线性关系,利用相邻数据点中的非缺失值来计算缺失数据点的值。
# 参数limit_direction: 表示线性填充时, 参考哪些值(forward: 向前, backward:向后, both:前后均参考)
city_day.interpolate(limit_direction="both")[50:64]['Xylene']
总结
- 文章介绍了数据拼接和缺失值处理的相关知识。