Pandas 数据处理进阶:增删改查、缺失值、合并与分组

前言

这是很多初学者在掌握 pd.read_csv() 和 df.head() 之后的真实困境。因为真实数据从来不是干干净净、整整齐齐的:

有缺失值(有的年份 GDP 没记录);

有重复行(数据不小心追加了两遍);

需要合并多张表(GDP 数据一张表,人口数据另一张表);

需要分组统计(分别看中国、日本、美国各自的 GDP 趋势)。

本文是 Pandas 系列的第三篇,在前两篇(数据结构与索引、基本操作与运算)的基础上,进入真实数据清洗与聚合分析的核心领域。


DataFrame数据的增删改查操作

导包并加载数据:

python 复制代码
import pandas as pd
data = pd.read_csv('./1960-2019全球GDP数据.csv',encoding='gbk')
print(data.head())

增加列

方式一: 通过直接赋值的方式添加新列

python 复制代码
# 拷贝一份data
data_copy = data.copy()
# 新增一列固定值
data_copy['new_col_1'] = 33
# 新增列数据数量必须和行数相等(可以用range或已有列计算)
# 假设data的行数为len(data)
data_copy['new_col_2'] = range(len(data))  # 或 [i for i in range(len(data))]
# 新增列使用已有列计算(比如年份*2,假设有year列)
data_copy['new_col_3'] = data_copy['year'] * 2
# 查看新增列后的df和原df
print(data_copy.head())
print(data.head())

方式二: data.assign函数添加列

python 复制代码
# 1.新增一列固定值
dataOne = data.assign(new0=88)
print(dataOne[['country', 'year', 'new0']].head())
python 复制代码
# 2. 新增一列,使用一组数据(数量必须与行数相等)
# 写死 [1,2,3,4,5] 会报错,这里动态生成等长序列
dataTwo = data.assign(new1=list(i for i in range(len(data))))
print("\n新增序号列后,前5行:")
print(dataTwo.head())
python 复制代码
# 3. 新增一列,使用 Series 对象(索引会自动对齐)
#    创建一个索引与 data 相同的 Series(默认整数索引一致)
series = pd.Series(range(len(data)),index=data.index)
dataThree= data.assign(new2=series)
print(dataThree[['country', 'year', 'new2']].head())
python 复制代码
dataFour = data.assign(new3=data['year']+data['GDP'])
print("\n新增计算列 year+GDP 后:")
print(dataFour[['country', 'year', 'new3']].head())
python 复制代码
def add(data):
    # 该函数必须返回一个标量、一个与 df 等长的序列,或一个索引与 df 一致的 Series
    print(f"\n自定义函数被调用,传入 data 的形状: {data.shape}")
    # 这里我们返回索引值(与 data 行数一致)
    return data.index.values  # 返回 numpy 数组

dataFive = data.assign(new4=add(data))
print("\n新增自定义函数列 new4(即索引值)后:")
print(dataFive[['country', 'year', 'new4']].head())
# 额外:查看原 data 是否被修改(验证 assign 不改变原数据)
print("\n原始 data 仍然只有原始列:", data.columns.tolist())

df.assign函数可以同时添加多列

python 复制代码
def foo(data):
    return 11

def bar(data):
    return data.year+5

dataTwo = data.assign(
    new0='heiehiheiheihei',
    new1=list(range(len(data))),
    new2=pd.Series(range(len(data)),index=data.index),
    new3=data.year*2,
    new4=foo(data),
    new5=bar(data)
)
print(dataTwo[['country', 'year','GDP', 'new0', 'new1', 'new2', 'new3', 'new4', 'new5']].head())

删除与去重

data.drop删除行数据

python 复制代码
data.drop([0]) # 默认删除行
data.drop([0, 2, 4]) # 可以删除多行
data.GDP.drop([0, 2]) # 对series对象按索引删除

data.drop删除列数据

  • data.drop默认删除指定索引值的行;如果添加参数axis=1,则删除指定列名的列
python 复制代码
data.drop(['new col 3'], axis=1)

使用del删除指定的列

  • 注意区别:
    • del是直接永久删除原df中的列【慎重使用】
    • drop是返回删除后的df或seires,原df或seires没有被修改
python 复制代码
del data['new col 3']

Dataframe数据去重

python 复制代码
import pandas as pd

data = pd.read_csv('./1960-2019全球GDP数据.csv',encoding='gbk')
print(f"原始数据行数: {len(data)}")
dataTwo = pd.concat([data, data], ignore_index=True)
print(f"追加自身后行数: {len(dataTwo )}")  # 应该是原始行数的两倍

# 注意:drop_duplicates() 默认不修改原数据,需要重新赋值或加 inplace=True
dataThree = dataTwo .drop_duplicates()  # 返回去重后的新 DataFrame
print(f"去重后行数: {len(dataThree)}")  # 应该等于原始行数(因为重复行被删除了)

series去重

python 复制代码
方式一:
data.country.drop_duplicates()
# 返回结果如下
0    美国
1    英国
2    法国
3    中国
4    日本
Name: country, dtype: object


方式二:
data.country.unique()
# 返回结果如下
array(['美国', '英国', '法国', '中国', '日本'], dtype=object)

修改DataFrame中的数据

data.assign替换列

python 复制代码
dataTwo = data.head(5)
print(dataTwo)
dataThree = dataTwo.assign(GDP=66)
print(dataThree)

直接对原始的DF进行赋值修改处理

  • 一般不建议直接修改操作
python 复制代码
data = pd.read_csv('../数据集/1960-2019全球GDP数据.csv', encoding='gbk', )  
data_new = data.head()
data_new
data_new['GDP'] = [5, 4, 3, 2, 1]
data_new
data # 此时原始的df会发生改变

replace函数替换数据

python 复制代码
import pandas as pd

# 读取数据,选取前5行作为一个新的 DataFrame
data = pd.read_csv('../数据集/1960-2019全球GDP数据.csv', encoding='gbk')
dataSix = data.head()
dataSix

# Series 对象替换数据,返回的还是 Series 对象,不会对原来的 df 造成修改
dataSix['year'].replace(1960, 19600)

# 如果加上 inplace=True 参数,则会修改原始 Series(进而修改 dataSix)
dataSix['country'].replace('日本', '扶桑', inplace=True)
dataSix

# DataFrame 也可以直接调用 replace 函数,用法和 Series.replace 用法一致,只是返回的是 DataFrame 对象
dataSix.replace(1960, 19600)
dataSix

查询dataFrame中的数据

从前从后取多行数据

head()

python 复制代码
# 导包 
import pandas as pd
# 加载csv数据,指定gbk编码格式来读取文件,返回df
data = pd.read_csv('../数据集/1960-2019全球GDP数据.csv', encoding='gbk') 

# 默认取前5行数据
data.head()
data.head(10) # 取前10行

tail()

python 复制代码
# 默认取后5行数据
data.tail()
dataTwo = df.tail(15) # 倒数15行
print(dataTwo)

获取一列或多列数据

获取一列数据data[col_name]等同于data.col_name

python 复制代码
data['GDP']
data.GDP
# 注意!如果列名字符串中间有空格的,只能使用df['country']这种形式

获取多列数据df[[col_name1,col_name2,...]]

python 复制代码
data[['country', 'GDP']] # 返回新的data

索引下标切片取行

data[start:stop:step]:

data[start:stop:step] == df[起始行下标:结束行下标:步长] , 遵循顾头不顾尾原则(包含起始行,不包含结束行),步长默认为1

python 复制代码
dataFour = data.head(10) # 取原data前10行数据作为dataFour,默认自增索引由0到9
dataFour[0:3] # 取前3行
dataFour[:5:2] # 取前5行,步长为2
dataFour[1::3] # 取第2行到最后所有行,步长为3

查询函数获取子集: data.query()

  • data.query(判断表达式)可以依据判断表达式返回的符合条件的data子集
  • data[布尔值向量]效果相同
  • 特别注意data.query()中传入的字符串格式
python 复制代码
data.query('country=="帕劳"')
data[data['country']=='帕劳']

查询中国, 美国 日本 三国 2015年至2019年的数据

python 复制代码
data.query('country=="中国" or country=="日本" or country=="美国"').query('year in [2015, 2016, 2017, 2018, 2019]'
data.query('(country=="中国" or country=="日本" or country=="美国") and year in [2015, 2016, 2017, 2018, 2019]')

排序函数

sort_values函数: 按照指定的一列或多列的值进行排序

python 复制代码
# 按GDP列的数值由小到大进行排序
data.sort_values(['GDP'])
# 按GDP列的数值由大到小进行排序
data.sort_values(['GDP'], ascending=False) # 倒序, ascending默认为True
# 先对year年份进行由小到大排序,再对GDP由小到大排序
data.sort_values(['year', 'GDP'])
  • rank函数:
  • rank函数用法:DataFrame.rank()Series.rank()
  • rank函数返回值:以Series或者DataFrame的类型返回数据的排名(哪个类型调用返回哪个类型)
  • rank函数包含有6个参数:
  • axis:设置沿着哪个轴计算排名(0或者1),默认为0按纵轴计算排名
  • numeric_only:是否仅仅计算数字型的columns,默认为False
  • na_option :NaN值是否参与排序及如何排序,固定参数:keep top bottom
    • keep: NaN值保留原有位置
    • top: NaN值全部放在前边
    • bottom: NaN值全部放在最后
  • ascending:设定升序排还是降序排,默认True升序
  • pct:是否以排名的百分比显示排名(所有排名与最大排名的百分比),默认False
  • method:排名评分的计算方式,固定值参数,常用固定值如下:
    • average : 默认值,排名评分不连续;数值相同的评分一致,都为平均值
    • min : 排名评分不连续;数值相同的评分一致,都为最小值
    • max : 排名评分不连续;数值相同的评分一致,都为最大值
    • dense : 排名评分是连续的;数值相同的评分一致
python 复制代码
data.rank()
python 复制代码
data.rank(axis=0)

列内排名

python 复制代码
data.rank(numeric_only=True)# 只对数值类型的列进行统计)
python 复制代码
data.rank(ascending=False) # 降序
python 复制代码
data.rank(pct=True) 
# 以最高分作为1,放回百分数形式的评分,pct参数默认为False
python 复制代码
data.rank(method='average')
data.rank(method='min')
data.rank(method='max')
data.rank(method='dense')

method='average':默认值。为并列值分配平均排名。

method='min':为并列值分配最小的排名。

method='max':为并列值分配最大的排名。

method='dense':分配排名,使得下一个值在上一名次上+1,且不跳号(最密集)。

python 复制代码
import pandas as pd
s = pd.Series([10, 20, 20, 30])

print(s.rank(method='average'))   # 输出: [1.0, 2.5, 2.5, 4.0]
print(s.rank(method='min'))       # 输出: [1.0, 2.0, 2.0, 4.0]
print(s.rank(method='max'))       # 输出: [1.0, 3.0, 3.0, 4.0]
print(s.rank(method='dense'))     # 输出: [1.0, 2.0, 2.0, 3.0]

聚合函数:

常用聚合函数有:

  • min 最小值
  • max 最大值
  • mean 平均值
  • sum 求和
  • count 求个数
python 复制代码
data['year'].min()
python 复制代码
data.min()

高级处理-缺失值处理

如何处理NAN

  • 获取缺失值的标记方式(NaN或者其他标记方式)
  • 如果缺失值的标记方式是NaN
    • 判断数据中是否包含NaN:
      • pd.isnull(df),
      • pd.notnull(df)
    • 存在缺失值nan:
      • 1、删除存在缺失值的:dropna(axis='rows')
        • 注:不会修改原数据,需要接受返回值
      • 2、替换缺失值:fillna(value, inplace=True)
        • value:替换成的值
        • inplace:True:会修改原数据,False:不替换修改原数据,生成新的对象
  • 如果缺失值没有使用NaN标记,比如使用"?"
    • 先替换'?'为np.nan,然后继续处理

电影数据的缺失值处理

电影数据文件获取

python 复制代码
df = pd.read_csv('./movie.csv')

判断缺失值是否存在

python 复制代码
pd.notnull(df)

np.all(pd.notnull(df)) 的作用是:检查 DataFrame(或 Series)中是否所有单元格都不是空值(NaN)。如果全部有数据,返回 True;只要存在一个缺失值(NaN),就返回 False。

Pandas 提供了两种完全等价的写法,你可以混用:

  • pd.isnull(df)(函数式风格)
  • df.isnull()(对象方法风格,更推荐)

存在缺失值nan,并且是np.nan

删除

pandas删除缺失值,使用dropna的前提是,缺失值的类型必须是np.nan

python 复制代码
# 不修改原数据
df.dropna()

# 可以定义新的变量接受或者用原来的变量名
data = df.dropna()

获取空值行 => ① row_with_null = df.isnull().any(axis=1) ② dfrow_with_null

  • df.isnull():标记出每一个空单元格(True 表示空)。
  • .any(axis=1):沿着行(横向)看,只要这一行里有一个 True,该行就标记为 True。返回一个布尔型 Series。
  • dfrow_with_null:通过布尔索引,把那些有问题的"坏行"全部筛选出来展示。

替换缺失值

python 复制代码
# 替换存在缺失值的样本的两列
# 替换填充平均值,中位数
df['Revenue (Millions)'].fillna(df['Revenue (Millions)'].mean(), inplace=True)
  • df'Revenue (Millions)':选择 DataFrame df 中的一列(Series)。
  • .fillna(...):Pandas Series/DataFrame 的一个方法,用于填充缺失值(NaN)。
  • df'Revenue (Millions)'.mean():fillna 方法的参数,计算同一列的平均值,用作填充值。
  • inplace=True:fillna 的一个参数,指示是否修改原始 DataFrame(df),而不是返回副本。

替换所有缺失值

python 复制代码
for i in df.columns:
    if np.all(pd.notnull(df[i])) == False:
        print(i)
        df[i].fillna(df[i].mean(), inplace=True)

不是缺失值nan,有默认标记的

python 复制代码
wis = pd.read_csv("https://archive.ics.uci.edu/ml/machine-learning-databases/breast-cancer-wisconsin/breast-cancer-wisconsin.data")

数据在读取时,可能会报如下错误:

python 复制代码
URLError: <urlopen error [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed (_ssl.c:833)>

解决办法:

python 复制代码
# 全局取消证书验证
import ssl
ssl._create_default_https_context = ssl._create_unverified_context

处理思路分析

  • 1、先替换'?'为np.nan
    • df.replace(to_replace=, value=)
      • to_replace:替换前的值
      • value:替换后的值
python 复制代码
# 把一些其它值标记的缺失值,替换成np.nan
wis = wis.replace(to_replace='?', value=np.nan)

2、在进行缺失值的处理

python 复制代码
# 删除
wis = wis.dropna()

小结

  • isnull、notnull判断是否存在缺失值
    • np.any(pd.isnull(movie)) # 里面如果有一个缺失值,就返回True
    • np.all(pd.notnull(movie)) # 里面如果有一个缺失值,就返回False
  • dropna删除np.nan标记的缺失值
    • movie.dropna()
  • fillna填充缺失值
    • moviei.fillna(value=moviei.mean(), inplace=True)
  • replace替换具体某些值
    • wis.replace(to_replace="?", value=np.NaN)

高级处理-数据合并

pd.concat实现数据合并

pd.concat(data1, data2, axis=1)

  • 按照行或列进行合并,axis=0为列索引,axis=1为行索引
python 复制代码
# 按照行索引进行
pd.concat([data, dummies], axis=1)

data, dummies:一个列表,包含要拼接的两个 DataFrame。

axis=1:指定拼接方向。

  • axis=0(默认)是纵向拼接(增加行)。
  • axis=1 是横向拼接(增加列)。

pd.merge

pd.merge(left, right, how='inner', on=None)

  • 可以指定按照两组数据的共同键值对合并或者左右各自
  • left: DataFrame
  • right: 另一个DataFrame
  • on: 指定的共同键
  • how:按照什么方式连接
python 复制代码
left = pd.DataFrame({'key1': ['K0', 'K0', 'K1', 'K2'],
                        'key2': ['K0', 'K1', 'K0', 'K1'],
                        'A': ['A0', 'A1', 'A2', 'A3'],
                        'B': ['B0', 'B1', 'B2', 'B3']})

right = pd.DataFrame({'key1': ['K0', 'K1', 'K1', 'K2'],
                        'key2': ['K0', 'K0', 'K0', 'K0'],
                        'C': ['C0', 'C1', 'C2', 'C3'],
                        'D': ['D0', 'D1', 'D2', 'D3']})

# 默认内连接
result = pd.merge(left, right, on=['key1', 'key2'])

左连接

python 复制代码
result = pd.merge(left, right, how='left', on=['key1', 'key2'])

右连接

python 复制代码
result = pd.merge(left, right, how='right', on=['key1', 'key2'])

外连接

python 复制代码
result = pd.merge(left, right, how='outer', on=['key1', 'key2'])

高级处理-数据分组

数据准备

加载优衣库的销售数据集,包含了不同城市优衣库门店的所有产品类别的销售记录,数据字段说明如下

  • store_id 门店随机id
  • city 城市
  • channel 销售渠道 网购自提 门店购买
  • gender_group 客户性别 男女
  • age_group 客户年龄段
  • wkd_ind 购买发生的时间(周末,周间)
  • product 产品类别
  • customer 客户数量
  • revenue 销售金额
  • order 订单数量
  • quant 购买产品的数量
  • unit_cost 成本(制作+运营)
python 复制代码
# 导包 加载数据集
import pandas as pd 
df = pd.read_csv('../数据集/uniqlo.csv')

groupby分组聚合

df.groupby分组函数返回分组对象

【基于一列进行分组】

python 复制代码
gs = df.groupby(['gender_group'])
print(gs)
print(gs['city'])

【基于多列进行分组】

python 复制代码
gs = df.groupby(['gender_group', 'city'])
print(gs)

分组后获取各个组内的数据

  • 2.1 取出每组第一条或最后一条数据
python 复制代码
gs = df.groupby(['gender_group', 'channel'])
print(gs.first())# 取出每组第一条数据
print(gs.last())  # 取出每组最后一条数据

分组后获取所有组的数据

python 复制代码
gs = df.groupby(['gender_group', 'channel'])
for name, group in gs:
   print(f"组名: {name}")
  print(f"该组数据形状: {group.shape}")
   print(group)  
   print("-" * 30)

按分组依据获取其中一组

python 复制代码
gs.get_group(('Female', '线上'))

分组聚合

格式:分组后对多列分别使用不同的聚合函数

python 复制代码
df.groupby(['列名1', '列名2']).agg({
    '指定列1':'聚合函数名', 
    '指定列2':'聚合函数名', 
    '指定列3':'聚合函数名'
})
  • 按城市和线上线下划分,分别计算销售金额的平均值、成本的总和
python 复制代码
df.groupby(['city', 'channel']).agg({
    'revenue':'mean', 
    'unit_cost':'sum'
})

分组过滤操作

格式:

python 复制代码
df.groupby(['列名1',...]).filter(
    lambda x: dosomething returun True or False
)

案例: 按城市分组,查询每组销售金额平均值大于200的全部数据

python 复制代码
df.groupby(['city']).filter(lambda s: s['revenue'].mean() > 200)
df.groupby(['city'])['revenue'].filter(lambda s: s.mean() > 200)

总结

希望本文能成为你数据清洗与分组分析路上的可靠指南。遇到任何具体的数据处理难题,欢迎随时查阅这篇"参数地图"。🚀