yield-bytes

沉淀、分享与无限进步

利用pandas将分组后的组内多行归并为一行

  本文的使用场景相对常见,例如需要解析一份含几千上万行Excel表(或者长报告里需要插入表格数据),需要将某列的一个单元格长字符串对应展开为多行且索引号不变,或者groupby后将同组的多行值拼接为单个长字符串,使用pandas可以快速完成。(文章由jupyter notebook导出的Markdown文件生成)

groupby后将同组的多行值连接为一个长字符串

使用df.groupby、apply、pd.merge

(在mysql中可以直接使用GROUP_CONCAT(字段 SEPARATOR ‘、’)方法完成)

1
2
3
4
5
6
7
import pandas as pd
data={
'省份':['上海','广东','上海','广东','上海'],
'大学名称':['复旦大学','中山大学','同济大学','华南理工大学','上海交通大学']
}
df=pd.DataFrame(data)
df
省份 大学名称
0 上海 复旦大学
1 广东 中山大学
2 上海 同济大学
3 广东 华南理工大学
4 上海 上海交通大学
1
2
3
4
flatten_rows=lambda s:'、'.join(s) # 这就是分组后,将组内多行合并为一行的处理逻辑,这里用顿号连接行字符串
# 使用apply方法,as_index=False,表示不将省份作为groupby的行索引
df1=df.groupby('省份',as_index=False)['大学名称'].apply(flatten_rows)
df1
省份 大学名称
0 上海 复旦大学、同济大学、上海交通大学
1 广东 中山大学、华南理工大学
1
2
df2=df.groupby('省份',as_index=False)['大学名称'].agg('count')
df2
省份 大学名称
0 上海 3
1 广东 2
1
2
# 将数量统计一列合并到df1,用省份关联即可,指定合并后,非key列的后缀,以便区分列来源哪个dataframe
df1.merge(df2,on='省份',suffixes=['_df1','_df2'])
省份 大学名称_df1 大学名称_df2
0 上海 复旦大学、同济大学、上海交通大学 3
1 广东 中山大学、华南理工大学 2
1
2
merged_df=pd.merge(df1,df2,on='省份',suffixes=['_df1','_df2']) # 也可直接用pd.merge方法
merged_df
省份 大学名称_df1 大学名称_df2
0 上海 复旦大学、同济大学、上海交通大学 3
1 广东 中山大学、华南理工大学 2
1
2
merged_df.columns=['省份','大学列表','数量']
merged_df
省份 大学列表 数量
0 上海 复旦大学、同济大学、上海交通大学 3
1 广东 中山大学、华南理工大学 2

df.groupby和agg方法

1
2
groupby_df=df.groupby('省份',as_index=False).agg({'大学名称': [flatten_rows,'count']})
groupby_df
省份 大学名称
<lambda_0> count
0 上海 复旦大学、同济大学、上海交通大学 3
1 广东 中山大学、华南理工大学 2
1
2
groupby_df.columns=['省份','大学列表','数量']
groupby_df
省份 大学列表 数量
0 上海 复旦大学、同济大学、上海交通大学 3
1 广东 中山大学、华南理工大学 2

同组一行长字符串展开为多行

此内容为前面的逆向处理(若使用mysql,需较为复杂的sql语句实现)

1
2
3
4
5
6
data2={
'省':pd.Series(['上海','广东','广西']),
'大学列表':pd.Series(['复旦大学、同济大学、上海交通大学','中山大学、华南理工大学',None])
}
df=pd.DataFrame(data2)
df
大学列表
0 上海 复旦大学、同济大学、上海交通大学
1 广东 中山大学、华南理工大学
2 广西 None
1
2
3
df21=df.copy()
# 创建数据集要小心,用pd.Series创建,则每行数据为字符串类型
df21['大学列表'][0]

使用pd.explode方法快速将一行展开为多行

1
# pd.explode要求单元格的值为一个列表,如['复旦大学', '同济大学', '上海交通大学'],因此需原单元格长字符串做处理
1
2
3
4
def split_func(cell): # 每个cell的长字符串分割,注意对于空值的处理
if not cell:
return cell
return cell.split('、')
1
2
df21['大学列表']=df21['大学列表'].apply(split_func) # 大学列表这一列的每行值都是字符串类型
df21['大学列表'][0]
['复旦大学', '同济大学', '上海交通大学']
1
df21.explode('大学列表',ignore_index=False) #ignore_index=False使得DataFrame对象保持原一行对应的索引号
大学列表
0 上海 复旦大学
0 上海 同济大学
0 上海 上海交通大学
1 广东 中山大学
1 广东 华南理工大学
2 广西 None

以上重点解析:对cell单元格的长字符串进行分割为一个列表,需自行设计一个分割函数
例如字符串:’复旦大学、同济大学、上海交通大学’
同理其他更为复杂的字符串:”黄小明201、李小明202|黄小明203%…”,采用正则分割即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# 关于pandas的explode几点说明
"""
pandas.DataFrame.explode()内部调用pandas.Series.explode():
result = df[column].explode()
# 删除原df指定展开列后,再与该列单独使用pandas.Series.explode得到的结果进行索引关联合并
result = df.drop([column], axis=1).join(result)

pandas.Series.explode()内部使用reshape.explode
values, counts = reshape.explode(np.asarray(self.array))
# 该方法来pandas._libs基础库目录的reshape.cpython-37m-darwin.so*,这里已经被编译为动态链接库

# pandas.Series.explode()方法表示将一列中单元格为列表值展开为多行

>>> s = pd.Series([[1, 2, 3], 'foo', [], [3, 4]])
>>> s
0 [1, 2, 3]
1 foo
2 []
3 [3, 4]
dtype: object

>>> s.explode()
0 1
0 2
0 3
1 foo
2 NaN
3 3
3 4
dtype: object

# 可以同一单元格展开多行后,索引号跟原单元格索引号相同,因此可用原df通过join展开多行的Series,就能快速完成全表展开。

"""

使用stack+join方法快速将一行展开为多行

1
2
df22=df.copy()
df22
大学列表
0 上海 复旦大学、同济大学、上海交通大学
1 广东 中山大学、华南理工大学
2 广西 None
1
2
df22_1=df22['大学列表'].str.split('、',expand=True) # 将一个cell值扩展为多列的cell值,该方法能自动处理cell空值
df22_1
0 1 2
0 复旦大学 同济大学 上海交通大学
1 中山大学 华南理工大学 None
2 None None None
1
2
3
#三、将扩展的列转成多行,有些列的单元格值为None,转为行记录需要去掉
df22_2=df22_1.stack(dropna=True)
df22_2
0  0      复旦大学
   1      同济大学
   2    上海交通大学
1  0      中山大学
   1    华南理工大学
dtype: object
1
df22_2.index
MultiIndex([(0, 0),
            (0, 1),
            (0, 2),
            (1, 0),
            (1, 1)],
           )
1
# 观察以上索引,若能把MultiIndex的第1列索引留下,则可关联到原表的0,1索引号
1
2
df22_2.reset_index(level=1,drop=True,inplace=True)
df22_2
0      复旦大学
0      同济大学
0    上海交通大学
1      中山大学
1    华南理工大学
dtype: object
1
2
df22_2.name='new_col'
df22_2
0      复旦大学
0      同济大学
0    上海交通大学
1      中山大学
1    华南理工大学
Name: new_col, dtype: object
1
del(df22['大学列表']) # 删除原数据集大学列表这一列,用于后面合并
1
2
df22_3=df22.join(df22_2) # 默认使用两个df的索引号进行关联合并。最终完成将一行.注意df22是dataframe才有join方法,df22_2是Series,没有join方法
df22_3
new_col
0 上海 复旦大学
0 上海 同济大学
0 上海 上海交通大学
1 广东 中山大学
1 广东 华南理工大学
2 广西 NaN
1
# 主要思路:将需要展开为多行的单元格值展开为多列,通过旋转操作将其变为一列Series,重置索引号,最终使用两个df索引号进行join关联合并,除了用pd.join,还可使用pd.concat两个省列与大学名称列关联合并为一张dataframe
1
2
3
# df22['省']属于Series类型,df22_2也是Series类型,两者通过索引号进行关联后,按列方向合并
df22_3=pd.concat([df22['省'],df22_2],join='inner',axis=1)
df22_3
new_col
0 上海 复旦大学
0 上海 同济大学
0 上海 上海交通大学
1 广东 中山大学
1 广东 华南理工大学
1
2
df22_3.columns=['省','大学名称']
df22_3 # 注意到广西所在行已被删除,最终可将None行垂直合并到df22_3即可
大学名称
0 上海 复旦大学
0 上海 同济大学
0 上海 上海交通大学
1 广东 中山大学
1 广东 华南理工大学

自行设计合并逻辑

将以下两行数据行

1
2
上海	复旦大学、同济大学、上海交通大学
广东 中山大学、华南理工大学

对应生成两个Series列数据

1
2
['上海','上海','上海','广东','广东']
['复旦大学','同济大学','上海交通大学','中山大学','华南理工大学']

再将这两个Series生成Dataframe即可

1
2
df23=df.copy()
df23
大学列表
0 上海 复旦大学、同济大学、上海交通大学
1 广东 中山大学、华南理工大学
2 广西 None
1
2
3
4
5
6
7
8
9
10
11
12
13
14
province_list=[] # 存放省的Series的值
college_list=[] # 存放大学名称Series的值
for row in df23.itertuples():
prov_cell=row[1]
split_cell=row[2]
if split_cell:
split_cell_list=split_cell.split('、')
for item in split_cell_list:
province_list.append(prov_cell)
college_list.append(item)
else: # 注意对cell为空值得处理
province_list.append(prov_cell)
college_list.append(split_cell)

1
province_list
['上海', '上海', '上海', '广东', '广东', '广西']
1
college_list
['复旦大学', '同济大学', '上海交通大学', '中山大学', '华南理工大学', None]
1
2
3
4
5
data23={
'省':province_list,
'大学名称':college_list
}
pd.DataFrame(data23) # 将以上两列Series拼接为一个df
大学名称
0 上海 复旦大学
1 上海 同济大学
2 上海 上海交通大学
3 广东 中山大学
4 广东 华南理工大学
5 广西 None