深度解密 pandas 的行列转换,以及一行变多行、一列变多列

简介: 深度解密 pandas 的行列转换,以及一行变多行、一列变多列


楔子




在用 pandas 做数据处理的时候,少不了对数据进行格式上的转换,但有些转换还是让人比较头疼的。下面就来介绍一下我认为比较复杂、但又很常见的格式转换需求,并给出相应的最佳解决方案。


行转列




假设有这样一种数据:

我们需要变成下面这种格式:

如果是你的话,你会怎么做呢?这里推荐两种方式。

首先 Series 对象有一个方法叫 unstack,调用一个具有二级索引的 Series 对象的 unstack 方法,会得到一个 DataFrame对象。其索引就是 Series 对象的一级索引,列就是 Series 对象的二级索引。

print(df)
"""
     姓名    科目  成绩
0  古明地觉  语文   90
1  古明地觉  数学   95
2  古明地觉  英语   96
3  芙兰朵露  语文   87
4  芙兰朵露  数学   92
5  芙兰朵露  英语   98
6   琪露诺   语文   100
7   琪露诺   数学   9
8   琪露诺   英语   91
"""
# 将 "姓名" 和 "科目" 设置为索引
# 然后取出 "成绩" 这一列
two_level_index_series = df.set_index(["姓名", "科目"])["成绩"]
# 此时得到的就是一个具有二级索引的 Series
# 一级索引是 "姓名" 这一列, 二级索引是 "科目" 这一列
print(two_level_index_series)
"""
姓名        科目
古明地觉    语文    90
            数学    95
            英语    96
芙兰朵露    语文    87
            数学    92
            英语    98
琪露诺      语文    100
            数学     9
            英语    91
Name: 成绩, dtype: int64
"""
# 调用具有二级索引的 Series的 unstack方法, 会得到一个 DataFrame
# 并自动把一级索引变成 DataFrame的索引, 二级索引变成 DataFrame的列
new_df = two_level_index_series.unstack()
print(new_df)
"""
科目     数学   英语   语文
姓名               
古明地觉  95     96     90
琪露诺    9      91    100
芙兰朵露  92     98     87
"""
# 格式是不是成功转换了呢? 
# 但是还有不完美的地方, 这个 DataFrame 的索引和列都有一个名字
# 索引的名字叫 "姓名", 列的名字叫 "科目"
# 因为原来 Series 对象的两个索引就叫 "姓名" 和 "科目"
# 可以通过 rename_axis(index=, columns=) 来给坐标轴重命名
new_df = new_df.rename_axis(columns=None)
# 这里我们只给列重命名, 没有给索引重命名, 至于原因请往下看
new_df = new_df.reset_index()
print(new_df)
"""
     姓名   数学  英语  语文
0  古明地觉  95    96    90
1   琪露诺   9     91   100
2  芙兰朵露  92    98    87
"""
# 大功告成, 如果索引的名字为空
# 那么 reset_index 之后, 列名就会变成字符串 "index"
# 但如果原来的索引有名字, 那么reset_index之后, 列名就是原来的索引名

所以调用 unstack 会将一级索引变成 DataFrame 的索引,二级索引变成 DataFrame 的列。更准确的说,unstack 是将最后一级的索引变成 DataFrame 的列,前面的索引变成 DataFrame 的索引。

比如一个具有八级索引的 Series,它在调用 unstack 的时候,默认是将最后一级索引变成 DataFrame 的列,前面七个索引整体作为 DataFrame 的索引。只不过索引一般很少有超过二级的,所以这里就用二级举例了。

那么问题来了,可不可以将一级索引"姓名"变成DataFrame的列,二级索引"科目"变成 DataFrame 的行呢?答案是可以的,在 unstack 中指定一个参数即可。

print(df)
"""
     姓名    科目  成绩
0  古明地觉  语文   90
1  古明地觉  数学   95
2  古明地觉  英语   96
3  芙兰朵露  语文   87
4  芙兰朵露  数学   92
5  芙兰朵露  英语   98
6   琪露诺   语文   100
7   琪露诺   数学   9
8   琪露诺   英语   91
"""
two_level_index_series = df.set_index(["姓名", "科目"])["成绩"]
# level 默认是 -1, 表示将最后一级索引变成列
# 这里我们指定为 0, 告诉 pandas, 把第一级索引变成列
new_df = two_level_index_series.unstack(level=0)
print(new_df)
"""
姓名  古明地觉  琪露诺  芙兰朵露
科目                 
数学    95        9       92
英语    96       91       98
语文    90      100       87
"""
new_df = new_df.rename_axis(columns=None)
new_df = new_df.reset_index()
print(new_df)
"""
   科目  古明地觉  琪露诺  芙兰朵露
0  数学    95        9       92
1  英语    96       91       98
2  语文    90      100       87
"""
# 我们看到结果就变了, 这种表示方式虽然有点奇怪
# 但它也确实可以正确地表达出数据的含义

如果稍微变通一下,会发现即使不使用 level=0 这个参数也可以实现这一点,那就是设置索引的时候不指定 ["姓名", "科目"],而是指定 ["科目", "姓名"],这样"科目"就成了一级索引,"姓名"就成了二级索引,一样可以完成任务。

除了 unstack 之外, pandas 还提供了一个模块级别的函数: pivot,可以让我们更加方便地处理这种数据。

print(df)
"""
     姓名    科目  成绩
0  古明地觉  语文   90
1  古明地觉  数学   95
2  古明地觉  英语   96
3  芙兰朵露  语文   87
4  芙兰朵露  数学   92
5  芙兰朵露  英语   98
6   琪露诺   语文   100
7   琪露诺   数学   9
8   琪露诺   英语   91
"""
print(
    pd.pivot(df, index="姓名", columns="科目", values="成绩")
)
"""
科目     数学   英语   语文
姓名               
古明地觉  95     96     90
琪露诺    9      91    100
芙兰朵露  92     98     87
"""
# 可以看到上面这一步
# 等价于 df.set_index(["姓名", "科目"])["分数"].unstack()
# 然后再手动 rename_axis、再 reset_index 即可
# 如果我们是想将 "姓名" 变成列的话, 那么就指定 columns="姓名" 即可
print(
    pd.pivot(df, index="科目", columns="姓名", values="成绩")
)
"""
姓名  古明地觉  琪露诺  芙兰朵露
科目                 
数学    95        9       92
英语    96       91       98
语文    90      100       87
"""

可以看到 pivot 算是 unstack 的一个很好的替代品,当然啦,两种方式都要掌握。


一行变多行




假设数据如下:

我们需要变成下面这种格式:

针对这种需求,pandas 同样提供了两种解决方式,先来看第一种。

print(df)
"""
          姓名        生日                      声优
0   琪亚娜·卡斯兰娜   12月7日                 陶典,钉宫理惠
1  布洛妮娅·扎伊切克  8月18日       TetraCalyx,Hanser,阿澄佳奈,花泽香菜
2  德丽莎·阿波卡利斯  3月28日                 花玲,田村由香里
"""
df = df.set_index(["姓名", "生日"])["声优"].str.split(",", expand=True)\
    .stack().reset_index(drop=True, level=-1).reset_index()\
    .rename(columns={0: "声优"})
print(df)
"""
          姓名        生日           声优
0   琪亚娜·卡斯兰娜   12月7日        陶典
1   琪亚娜·卡斯兰娜   12月7日      钉宫理惠
2  布洛妮娅·扎伊切克  8月18日     TetraCalyx
3  布洛妮娅·扎伊切克  8月18日       Hanser
4  布洛妮娅·扎伊切克  8月18日       阿澄佳奈
5  布洛妮娅·扎伊切克  8月18日       花泽香菜
6  德丽莎·阿波卡利斯  3月28日         花玲
7  德丽莎·阿波卡利斯  3月28日      田村由香里
"""

估计可能有人会懵,我们来一步一步拆解,不过需要先介绍一下 DataFrame 的 stack 方法。

首先 Series 只有 unstack,没有 stack。而 DataFrame 既有 unstack,又有 stack。Series(具有二级索引)的 unstack 是将自身变成一个 DataFrame,而 DataFrame 的 unstack 和 stack 则是将自身变成一个具有二级索引(前提是该DataFrame的索引和列都只有一级)的 Series。

我们上面说了,在默认情况下,对于 Series 来讲,调用其 unstack 方法,会默认将一级索引变成 DataFrame 的索引,二级索引变成 DataFrame 的列(当然可以通过 level 参数来控制)。

但是调用 DataFrame 的 unstack 则是将自身的索引变成对应 Series 的二级索引,将自身的列变成对应 Series 的一级索引;调用 DataFrame 的 stack,则是将自身的索引变成对应 Series 的一级索引,将自身的列变成对应 Series 的二级索引。

文字读起来绕的话,可以看一张图,在默认情况下如图所示:

下面我们就来分析之前的那一大长串。

print(df)
"""
          姓名         生日                      声优
0   琪亚娜·卡斯兰娜   12月7日                 陶典,钉宫理惠
1  布洛妮娅·扎伊切克  8月18日       TetraCalyx,Hanser,阿澄佳奈,花泽香菜
2  德丽莎·阿波卡利斯  3月28日                 花玲,田村由香里
"""
# 我们显然要对 "声优" 这个字段进行 split
# 那么将除了该字段之外的其它字段设置为索引
df = df.set_index(["姓名", "生日"])
# 此时的 df 只有 "声优" 这一列
# 原来的 "姓名" 和 "生日" 被设置成了索引
print(df)
"""  
                                               声优
姓名                   生日                      
琪亚娜·卡斯兰娜       12月7日                 陶典,钉宫理惠
布洛妮娅·扎伊切克     8月18日       TetraCalyx,Hanser,阿澄佳奈,花泽香菜
德丽莎·阿波卡利斯     3月28日                 花玲,田村由香里
"""
# 筛选出 "声优" 这个字段, 此时会得到一个具有二级索引的 Series
# 索引的名字叫做: "姓名" 和 "生日"
s = df["声优"]
print(s)
"""
姓名                   生日                      
琪亚娜·卡斯兰娜       12月7日                 陶典,钉宫理惠
布洛妮娅·扎伊切克     8月18日       TetraCalyx,Hanser,阿澄佳奈,花泽香菜
德丽莎·阿波卡利斯     3月28日                 花玲,田村由香里
Name: 声优, dtype: object
"""
# 然后进行分解, 我这里是以逗号为分隔符
# 具体是以什么分隔, 以你自己的数据为准
# 显然这一步会得到一个DataFrame
df = s.str.split(",", expand=True)
# split 之后, 字段名使用 0 1 2 3...
# 默认以最长为基准, 长度不够的使用None填充
print(df)
"""
                                 0          1         2       3
姓名                生日                                   
琪亚娜·卡斯兰娜    12月7日      陶典      钉宫理惠   None    None
布洛妮娅·扎伊切克  8月18日   TetraCalyx   Hanser  阿澄佳奈  花泽香菜
德丽莎·阿波卡利斯  3月28日      花玲     田村由香里   None    None
"""
# 调用 stack 方法, 按照之前说的, 会变成一个 Series
# 其索引就是 DataFrame 的索引再加上列变成的索引
# 由于 DataFrame 的索引有两级, 显然此时得到的是一个具有三级索引的 Series
# 不过也可以把 DataFrame 的索引当成一个整体看成是 Series 的一级索引
# 然后列变成对应的二级索引
s = df.stack()
# 此时的数据好像有那么回事了
# 另外我们发现在 stack 的时候自动将 None 给过滤掉了, 这也是我们希望的结果
print(s)
"""
姓名                 生日      
琪亚娜·卡斯兰娜     12月7日    0       陶典
                               1     钉宫理惠
布洛妮娅·扎伊切克   8月18日    0    TetraCalyx
                               1      Hanser
                               2      阿澄佳奈
                               3      花泽香菜
德丽莎·阿波卡利斯   3月28日    0        花玲
                               1     田村由香里
dtype: object
"""
# 然后调用 reset_index, 但由于索引有三级
# 那么reset_index之后就会使得 0 1 0 1... 这些也变成了一列
# 可以之后手动drop掉, 但是我们也可以直接删掉
s = s.reset_index(drop=True, level=-1)
# 于是指定 drop=True, 但是这样会把所有索引都删掉
# 因此还要指定 level=-1, 表示只删除最后一级索引
print(s)
"""
姓名                 生日      
琪亚娜·卡斯兰娜     12月7日       陶典
                                钉宫理惠
布洛妮娅·扎伊切克   8月18日    TetraCalyx
                                 Hanser
                                阿澄佳奈
                                花泽香菜
德丽莎·阿波卡利斯   3月28日       花玲
                               田村由香里
dtype: object
"""
# 上面的 reset_index(drop=True, level=-1) 并没有把前面的索引变成列
# 这是因为指定了level,如果不指定level,那么 drop=True 会把所有的索引都删掉
# 而指定了level只会删除对应级别的索引,但不会同时对前面的索引进行reset
# 于是需要再调用一次reset_index,此时就什么也不需要指定了
df = s.reset_index()
# 会自动进行笛卡尔乘积
print(df)
""" 
        姓名            生日          0
0   琪亚娜·卡斯兰娜    12月7日        陶典
1   琪亚娜·卡斯兰娜    12月7日      钉宫理惠
2  布洛妮娅·扎伊切克   8月18日    TetraCalyx
3  布洛妮娅·扎伊切克   8月18日       Hanser
4  布洛妮娅·扎伊切克   8月18日      阿澄佳奈
5  布洛妮娅·扎伊切克   8月18日      花泽香菜
6  德丽莎·阿波卡利斯   3月28日        花玲
7  德丽莎·阿波卡利斯   3月28日     田村由香里
"""
# 但列名是自动生成的 0,于是再进行rename
df = df.rename(columns={0: "声优"})
print(df)
"""
        姓名            生日         声优
0   琪亚娜·卡斯兰娜    12月7日        陶典
1   琪亚娜·卡斯兰娜    12月7日      钉宫理惠
2  布洛妮娅·扎伊切克   8月18日    TetraCalyx
3  布洛妮娅·扎伊切克   8月18日       Hanser
4  布洛妮娅·扎伊切克   8月18日      阿澄佳奈
5  布洛妮娅·扎伊切克   8月18日      花泽香菜
6  德丽莎·阿波卡利斯   3月28日        花玲
7  德丽莎·阿波卡利斯   3月28日     田村由香里
"""

整个过程还是不难的,核心就在于理解 DataFrame 的 stack 方法。当然啦,针对这种需求,pandas 在 0.25 版本的时候,给 DataFrame新增了一个 explode 方法,专门用来将一行变多行。

如果你用过 hive 的话,那么 explode 你肯定会很熟悉,它用来对一个数组进行"炸裂"。只不过 hive 中的 explode 在对数组进行炸裂的时候不可以有其它的字段,如果在炸裂的同时还能和其它字段进行笛卡尔积,那么还需要加上一个"侧写"的语法。

但是 pandas 要简单许多,explode 可以直接对 DataFrame 使用,我们来看一下:

print(df)
"""
          姓名         生日                      声优
0   琪亚娜·卡斯兰娜   12月7日                 陶典,钉宫理惠
1  布洛妮娅·扎伊切克  8月18日       TetraCalyx,Hanser,阿澄佳奈,花泽香菜
2  德丽莎·阿波卡利斯  3月28日                 花玲,田村由香里
"""
# 此时就不需要指定 expand=True 了
# 这里我们需要的是一个列表
df["声优"] = df["声优"].str.split(",")
print(df)
"""
          姓名         生日                      声优
0   琪亚娜·卡斯兰娜   12月7日                [陶典, 钉宫理惠]
1  布洛妮娅·扎伊切克  8月18日       [TetraCalyx, Hanser, 阿澄佳奈, 花泽香菜]
2  德丽莎·阿波卡利斯  3月28日                [花玲, 田村由香里]
"""
# 对该字段进行炸裂
df = df.explode("声优")
print(df)
"""
        姓名            生日          声优
0   琪亚娜·卡斯兰娜    12月7日        陶典
1   琪亚娜·卡斯兰娜    12月7日      钉宫理惠
2  布洛妮娅·扎伊切克   8月18日    TetraCalyx
3  布洛妮娅·扎伊切克   8月18日       Hanser
4  布洛妮娅·扎伊切克   8月18日      阿澄佳奈
5  布洛妮娅·扎伊切克   8月18日      花泽香菜
6  德丽莎·阿波卡利斯   3月28日        花玲
7  德丽莎·阿波卡利斯   3月28日     田村由香里
"""

我们看到直接就把这个字段给炸开了,并且炸开的同时会自动和其它字段进行笛卡尔积。所以 explode 这个功能真的是好用,而且这种数据在工作中也是非常常见的。


一列变多列




有时候我们也会将数据的某一列,拆分成多列,比如:

对于当前这个例子,调用 DataFrame 的 apply 方法是最快的解决办法。

print(df)
"""
id                                      info
001  {'姓名': '琪亚娜·卡斯兰娜', '生日': '12月7日', '外号': '草履虫'}
002  {'姓名': '布洛妮娅·扎伊切克', '生日': '8月18日', '外号': '板鸭'}
003  {'姓名': '德丽莎·阿波卡利斯', '生日': '3月28日', '外号': '德丽傻', '武器': '犹大的誓约'}
"""
# 筛选出 "info" 这一列, 然后使用 apply
tmp = df["info"].apply(pd.Series)
# 打印一看, 神奇的事情发生了, 直接就变成了我们想要的结果
print(tmp)
"""
        姓名            生日      外号        武器
0  琪亚娜·卡斯兰娜     12月7日    草履虫       NaN
1 布洛妮娅·扎伊切克    8月18日     板鸭        NaN
2 德丽莎·阿波卡利斯    3月28日    德丽傻    犹大的誓约
"""
# 因为这里的值是一个字典, 而Series接收一个字典的话
# 那么字典的 key 就是索引, value 就是值
# 在扩展成 DataFrame 的时候,key 就会变成列名
# 并且会考虑到字典中所有的 key, 有多少个不重复的key就会生成多少个列
# 如果列所在的行没有对应的值,则使用NaN填充
# 然后就简单了, 将 tmp 添加到 df 中
df[tmp.columns] = tmp
# 然后删掉"info"这一列
df = df.drop(columns=["info"])
print(df)
"""
id            姓名            生日      外号        武器
001      琪亚娜·卡斯兰娜     12月7日    草履虫       NaN
002     布洛妮娅·扎伊切克    8月18日     板鸭        NaN
003     德丽莎·阿波卡利斯    3月28日    德丽傻    犹大的誓约
"""

只需要对 info 这一列使用 apply(pd.Series),即可基于字典将一列拆分成多列。列名就是字典的 key,如果列所在的行没有值,则用 NaN 填充。

注意:使用 apply(pd.Series) 的时候,对应的列里面的值应该是一个字典,不能是字典格式的字符串。很多时候我们会从对方的接口、或者数据库中获取数据,但数据并不是字典,而是一个 JSON 字符串。那么这时候需要先调用 json.loads 将字符串变成字典,然后再调用 apply。

此外,使用 apply(pd.Series) 时,该列的值不仅仅可以是字典,还可以是元组或者列表等可迭代对象。

print(df)
"""
id                info
001   [琪亚娜·卡斯兰娜, 12月7日, 草履虫]
002   [布洛妮娅·扎伊切克, 8月18日, 板鸭]
003   [德丽莎·阿波卡利斯, 3月28日, 德丽傻, 犹大的誓约]
"""
tmp = df["info"].apply(pd.Series)
print(tmp)
"""
        0                1         2          3
0  琪亚娜·卡斯兰娜     12月7日    草履虫      NaN
1 布洛妮娅·扎伊切克    8月18日     板鸭       NaN
2 德丽莎·阿波卡利斯    3月28日    德丽傻    犹大的誓约
"""

效果是一样的,只不过列名变成了 0 1 2 3....

数据还可以更复杂,如果某一列的值都是列表,并且列表里面嵌套了字典,现在让你将该列拆分成多列的话,你会怎么做呢?很简单,先 explode 将列表炸开,然后再 apply(pd.Series) 进行拆分即可。

总的来说,pandas在处理这种数据时的表现是非常优秀的。


列转行




再来看一看列转行,列转行可以简单地认为是把数据库中的宽表变成一张高表;而上面的行转列则是把一张高表变成一张宽表。

假设有如下数据:

当然我们这里字段比较少,如果把星期一到星期日全部都写上去,有点太长了。不过从这里也能看出前者对应的是一张宽表,就是字段非常多,我们要将其转换成一张高表。

pandas提供了一个模块级的函数:melt,可以方便地实现这一点。

import pandas as pd
df = pd.DataFrame(
    {"姓名": ["古明地觉", "雾雨魔理沙", "琪露诺"],
     "水果": ["草莓", "樱桃", "西瓜"],
     "星期一": ["70斤", "61斤", "103斤"],
     "星期二": ["72斤", "60斤", "116斤"],
     "星期三": ["60斤", "81斤", "153斤"]})
print(df)
"""
   姓名       水果   星期一   星期二   星期三   
 古明地觉     草莓    70斤     72斤     60斤 
雾雨魔理沙    樱桃    61斤     60斤     81斤  
  琪露诺      西瓜    103斤    116斤    153斤
"""
print(pd.melt(df,
              id_vars=["姓名", "水果"],
              value_vars=["星期一", "星期二", "星期三"]))
"""
   姓名      水果     variable    value
 古明地觉    草莓      星期一      70斤
雾雨魔理沙   樱桃      星期一      61斤
  琪露诺     西瓜      星期一     103斤
 古明地觉    草莓      星期二      72斤
雾雨魔理沙   樱桃      星期二      60斤
  琪露诺    西瓜       星期二     116斤
 古明地觉    草莓      星期三      60斤
雾雨魔理沙   樱桃      星期三      81斤
  琪露诺     西瓜      星期三     153斤
"""
# 但是转换之后,字段名默认叫 variable 和 value
# 我们可以在结果的基础之上手动 rename, 也可以直接在参数中指定
print(pd.melt(df, id_vars=["姓名", "水果"],
              value_vars=["星期一", "星期二", "星期三"],
              var_name="星期几?",
              value_name="销量"))
"""
   姓名      水果      星期几?     销量
 古明地觉    草莓      星期一      70斤
雾雨魔理沙   樱桃      星期一      61斤
  琪露诺     西瓜      星期一     103斤
 古明地觉    草莓      星期二      72斤
雾雨魔理沙   樱桃      星期二      60斤
  琪露诺    西瓜       星期二     116斤
 古明地觉    草莓      星期三      60斤
雾雨魔理沙   樱桃      星期三      81斤
  琪露诺     西瓜      星期三     153斤
"""

实现起来非常方便,转换之后列变少了、行变多了,这个过程就是所谓的列转行;而反过来行变少了、列变多了,则叫做行转列。那么这里的 melt 是如何变换的呢?示意图如下:


如果还有其它字段的话,那么和 explode 一样,会自动进行笛卡尔积。

然后是 melt 里面的几个参数:

  • frame:第一个参数,接收一个DataFrame,这没有什么好说的;
  • id_vars:第二个参数,不需要进行列转行的字段,比如这里的"姓名"和"水果",在列转行之后会自动进行匹配;
  • value_vars:第三个参数,需要进行列转行的字段;
  • var_name:第四个参数,我们知道列转行之后会生成两个新的列,第一个列存储的是"进行列转行的列的名称",第二个列存储的是"进行列转行的列的值"。但是生成的两个列总要有列名吧,所以 var_name 就是生成的第一个列的列名;
  • value_name:生成的第二个列的列名;
  • col_level:针对于具有二级列名的 DataFrame,这个一般可以不用管;


另外,我们指定列的时候,也可以只指定一部分,比如:

import pandas as pd
df = pd.DataFrame(
    {"姓名": ["古明地觉", "雾雨魔理沙", "琪露诺"],
     "水果": ["草莓", "樱桃", "西瓜"],
     "星期一": ["70斤", "61斤", "103斤"],
     "星期二": ["72斤", "60斤", "116斤"],
     "星期三": ["60斤", "81斤", "153斤"]})
print(df)
"""
   姓名       水果   星期一   星期二   星期三   
 古明地觉     草莓    70斤     72斤     60斤 
雾雨魔理沙    樱桃    61斤     60斤     81斤  
  琪露诺      西瓜    103斤    116斤    153斤
"""
# id_vars 只指定 "水果"
# value_vars 只指定 ["星期一", "星期二"]
print(pd.melt(df, id_vars=["水果"],
              value_vars=["星期一", "星期二"],
              var_name="日期",
              value_name="销量"))
"""
   水果   日期    销量
0  草莓  星期一   70斤
1  樱桃  星期一   61斤
2  西瓜  星期一  103斤
3  草莓  星期二   72斤
4  樱桃  星期二   60斤
5  西瓜  星期二  116斤
"""
# 也可以指定字段顺序, 比如: "水果" 在前, "姓名" 在后
print(pd.melt(df, id_vars=["水果", "姓名"],
              value_vars=["星期一", "星期二"],
              var_name="日期",
              value_name="销量"))
"""
   水果     姓名     日期      销量
0  草莓   古明地觉   星期一    70斤
1  樱桃  雾雨魔理沙  星期一    61斤
2  西瓜    琪露诺   星期一    103斤
3  草莓   古明地觉  星期二     72斤
4  樱桃  雾雨魔理沙  星期二    60斤
5  西瓜    琪露诺   星期二    116斤
"""
# 如果要进行"列转行"的列比较多的话,可以只指定 id_vars
# 默认将剩余的所有列作为 value_vars
# 因此这里将除了"姓名"、"水果"之外的所有列都进行列转行了
print(pd.melt(df, id_vars=["水果", "姓名"],
              var_name="日期",
              value_name="销量"))
""" 
     姓名      水果      日期       销量
   古明地觉    草莓      星期一      70斤
  雾雨魔理沙   樱桃      星期一      61斤
    琪露诺     西瓜      星期一     103斤
   古明地觉    草莓      星期二      72斤
  雾雨魔理沙   樱桃      星期二      60斤
    琪露诺     西瓜      星期二     116斤
   古明地觉    草莓      星期三      60斤
  雾雨魔理沙   樱桃      星期三      81斤
    琪露诺     西瓜      星期三     153斤
"""
# 但是注意: 在value_vars省略的时候, 则需要仔细考虑一下id_vars
# 什么意思呢? 看个栗子
print(pd.melt(df, id_vars=["水果"],
              var_name="日期",
              value_name="销量"))
"""
    水果   日期     销量
0   草莓   姓名   古明地觉
1   樱桃   姓名  雾雨魔理沙
2   西瓜   姓名    琪露诺
3   草莓  星期一    70斤
4   樱桃  星期一    61斤
5   西瓜  星期一   103斤
6   草莓  星期二    72斤
7   樱桃  星期二    60斤
8   西瓜  星期二   116斤
9   草莓  星期三    60斤
10  樱桃  星期三    81斤
11  西瓜  星期三   153斤
"""
# 它将"姓名"这一列也进行列转行了, 但是显然我们不想这么做
# 所以在省略 value_vars 的时候, 务必注意 id_vars
# 因为它会将除了 id_vars 指定的字段之外的其它所有字段都进行列转行
# 假设这里要列转行的列很多, 不想一个一个写, 但是 id_vars 又不想指定 "姓名"
# 那么在列转行之前将 "姓名" 这一列删掉即可
print(pd.melt(df.drop(columns=["姓名"]),
              id_vars=["水果"],
              var_name="日期",
              value_name="销量"))
"""
   水果   日期    销量
0  草莓  星期一   70斤
1  樱桃  星期一   61斤
2  西瓜  星期一  103斤
3  草莓  星期二   72斤
4  樱桃  星期二   60斤
5  西瓜  星期二  116斤
6  草莓  星期三   60斤
7  樱桃  星期三   81斤
8  西瓜  星期三  153斤
"""

以上就是 melt 的用法,在进行列转行的时候非常方便。事实上,列转行除了使用 melt,还可以使用我们上面说的 stack,只不过 melt 会更加的方便。

我们看一下用 stack 如何实现。

import pandas as pd
df = pd.DataFrame(
    {"姓名": ["古明地觉", "雾雨魔理沙", "琪露诺"],
     "水果": ["草莓", "樱桃", "西瓜"],
     "星期一": ["70斤", "61斤", "103斤"],
     "星期二": ["72斤", "60斤", "116斤"]})
# 将不需要列转行的字段设置为索引
df = df.set_index(["姓名", "水果"])
print(df)
"""
                    星期一   星期二
姓名        水果            
古明地觉    草莓     70斤     72斤
雾雨魔理沙  樱桃     61斤     60斤
琪露诺      西瓜     103斤    116斤
"""
# 调用 stack 方法, 会得到一个具有多级索引的 Series
# DataFrame 的列就是生成的 Series 的最后一级索引
s = df.stack()
print(s)
"""
 姓名       水果     
古明地觉    草莓      星期一      70斤
                     星期二       72斤
雾雨魔理沙  樱桃      星期一      61斤
                     星期二       60斤
琪露诺      西瓜      星期一      103斤
                     星期二       116斤
dtype: object
"""
# 直接对该 Series 进行 reset_index 即可
# 会得到 DataFrame
df = s.reset_index()
print(df)
"""
      姓名     水果    level_2       0
0   古明地觉   草莓     星期一      70斤
1   古明地觉   草莓     星期二      72斤
2  雾雨魔理沙  樱桃     星期一      61斤
3  雾雨魔理沙  樱桃     星期二      60斤
4    琪露诺    西瓜     星期一      103斤
5    琪露诺    西瓜     星期二      116斤
"""
# 重命名
df.columns = ["姓名", "水果", "日期", "销量"]
print(df)
"""
      姓名     水果    日期     销量
0   古明地觉   草莓   星期一    70斤
1   古明地觉   草莓   星期二    72斤
2  雾雨魔理沙  樱桃   星期一    61斤
3  雾雨魔理沙  樱桃   星期二    60斤
4    琪露诺    西瓜   星期一   103斤
5    琪露诺    西瓜   星期二   116斤
"""

此时我们就通过 stack 方法实现了 pandas 的 melt 函数,对比 melt 函数实现的结果,我们发现顺序有些差异,但显然结果是一样的。所以,基于 stack 我们完全可以自己封装一个 melt 函数。


列的对称合并




假设有这样一种数据:

我们需要转成下面这种格式:

如果这种显示方式不直观的话,那么我们换一种,按照姓名排序一下:


怎么样,这样显示的话是不是就一眼看出我们接下来要做什么了呢?我们先来试一下之前的 melt 能否实现,看看使用 melt 会得到什么结果?

pd.melt(df, id_vars=["姓名", "年龄"])

显然这不是我们希望的结果,那应该怎么做呢?答案依旧是使用 melt,只不过需要做一些变换。当然,如果只能靠先变换数据、再使用 melt 的话,我这里就不会多此一举了。没错,pandas 针对这种情况专门提供了一个函数,非常的清蒸。至于怎么用,我们待会说,先来看看如何使用 melt 实现。


首先将 "技能1"、"技能2"、"技能3" 组合起来变成一个元组,赋值为 "技能",将 "活动地点1"、"活动地点2"、"活动地点3" 组合起来变成一个元组,赋值为 "活动地点"。然后使用 filter 方法将那些不需要的字段过滤掉,这个方法非常好用,里面支持正则筛选,(?<!\d)$ 表示筛选结尾不是数字的列。


将 "技能" 和 "活动地点" 组合起来得到 "技能-活动地点"。


通过 explode 将这个字段中的元素炸开,怎么样,此时离胜利是不是越近了呢?


大功告成,我们说除了可以对一个字典使用 apply(pd.Series) 之外,还可以对元组、列表使用,只不过它们会得到一个默认的字段名:0 1,这里我们直接赋值给 "技能"、"活动地点" 即可。至于里面的 "技能-活动地点" 这一列本身我们是不需要的,再给它删掉即可。

以上就是我们使用 melt 的方式实现,其实还是没有什么难度,早期遇见此类数据我也是按照这种办法做的,处理的时候还是很香的。那么下面重点来了,pandas 似乎知道我们会面临这种需求,所以提前帮我们想好了,于是提供了一个 pd.lreshape 函数来轻松地解决这一问题。


屮艸芔茻,简直绝了有木有,如果再按照 "姓名" 排个序的话,跟我们自己使用 melt 得到的结果完全是一样的,而且其它字段也会自动进行笛卡尔积。

说实话 pandas 发展到现在,基本上我们能遇到的问题别人也会遇到。而如果有什么好的想法的话,可以反馈给 pandas 社区,在下一个版本的时候没准也会加进去。所以发展到现在的 pandas 基本上能解决你的任何需求,至于 lreshape 这个方法什么时候加进去的个人没有考证,但是上面没有版本信息,估计应该很早就有了。

另外,进行组合的字段不能含有空值,否则这一整行就没了,举个例子:


如果想避免这一点,只需要将 dropna 参数设置为 False 即可。

此时这条记录就不会被删除了。


小结




以上就是一些经常会遇见,但是不熟悉的话处理起来又会感到麻烦的几种数据以及需求,再来总结一下:

行转列:

1)设置索引、筛选单个字段,得到一个具有二级索引的 Series;

2)调用 Series 的 unstack 方法,得到一个 DataFrame;

3)rename_axis;

4)reset_index。

另外还有一个 pd.pivot 函数,可以让我们直接跳转到上面的第三步。

一行变多行:

1)如果该列里面的元素不是列表,那么变成列表;

2)调用 explode 方法对该字段进行炸裂;

一列变多列:

1)如果该列的元素不是字典或者列表,那么转成字典或列表;

2)调用 .apply(pd.Series),如果元素是字典,那么生成的列的名字就是字典里面的 key。如果元素是列表,那么生成的列的名字就是默认的 0 1 2 3......;

如果元素是嵌套了字典的列表,那么 一行变多行 和 一列变多列 也可以结合起来,即:先 explode,再 apply;

列转行:

通过 pd.melt 一步搞定,或者调用 DataFrame 的 stack 方法;

列的对称合并:

通过 pd.lreshape 一步搞定,或者使用 pd.melt 手动实现。

因此针对行转列和列转行,早期我个人经常使用 unstack 和 stack 方法。但是之后就没怎么用了,因为 pd.pivot 和 pd.melt 函数算是 unstack 和 stack 的一个很好的替代品,可以帮助我们更快地实现。

另外除了行转列和列转行之外,基于列表实现一行生成多行、基于字典拆分成多列,pandas 也依旧提供了很棒的支持。

相关文章
|
5月前
|
SQL 数据采集 数据可视化
使用Python Pandas实现两表对应列相加(即使表头不同)
使用Python Pandas实现两表对应列相加(即使表头不同)
148 3
|
2天前
|
索引 Python
如何高效地对比处理 DataFrame 的两列数据
如何高效地对比处理 DataFrame 的两列数据
8 0
|
2月前
|
数据挖掘 大数据 BI
在pandas中使用数据透视表
在pandas中使用数据透视表
|
2月前
|
数据挖掘 Python
pandas中基于范围条件进行表连接
pandas中基于范围条件进行表连接
|
4月前
|
SQL 数据可视化 数据挖掘
Pandas透视表及应用(二)
这个文本是关于使用Pandas进行数据分析的教程,主要关注会员数据的处理和业务指标的计算。
|
4月前
|
监控 数据可视化 数据挖掘
Pandas透视表及应用(一)
数据透视表(Pivot Table)是一种交互式的表,可以进行某些计算,如求和与计数等。所进行的计算与数据跟数据透视表中的排列有关。
|
数据处理 数据库 Python
Pandas高级应用:数据透视表和字符串操作
Pandas是Python中用于数据处理和分析的强大库。这篇文章将深入探讨Pandas库的高级功能:数据透视表和字符串操作。
|
数据挖掘 索引 Python
【Python数据分析 - 9】:DataFrame结构中自定义行列索引(Pandas篇)
【Python数据分析 - 9】:DataFrame结构中自定义行列索引(Pandas篇)
502 0
【Python数据分析 - 9】:DataFrame结构中自定义行列索引(Pandas篇)
Pandas 按照两列分组后只选排序第一名
Pandas 按照两列分组后只选排序第一名
|
数据挖掘 数据处理 索引
Pandas索引学习
Pandas索引学习
120 0