Python数据分析NumPy和pandas(二十六、数据整理--连接、合并和重塑 之三:重塑和透视)
对表格数据的重新排列操作,称为 reshape 或 pivot 。有很多种方法对表格数据进行重塑。
一、使用分层索引进行reshape
分层索引提供了一种在 DataFrame 中重新排列数据的方法。主要有两个函数方法:
stack:将数据中的列旋转或透视到行。
unstack:从行转为列。
还是用代码示例来学习。用一个以字符串数组作为行和列索引的 DataFrame 做为操作示例:
import numpy as np
import pandas as pd
data = pd.DataFrame(np.arange(6).reshape((2, 3)),
index=pd.Index(["Ohio", "Colorado"], name="state"),
columns=pd.Index(["one", "two", "three"],
name="number"))
print(data)
# 使用 stack 方法将data的列转置到行中,从而生成一个 Series
result = data.stack()
print(result)
data输出:
number | one | two | three |
---|---|---|---|
state | |||
Ohio | 0 | 1 | 2 |
Colorado | 3 | 4 | 5 |
result输出:
state number
Ohio one 0
two 1
three 2
Colorado one 3
two 4
three 5
dtype: int32
从分层索引的 Series 中,可以使用 unstack 将数据重新排列回 DataFrame 中:result.unstack()
number | one | two | three |
---|---|---|---|
state | |||
Ohio | 0 | 1 | 2 |
Colorado | 3 | 4 | 5 |
默认情况下,对于分层索引数据unstack方法使用最内层层号或名称进行转置操作(stack也一样),但是我们也可以传递参数level给unstack方法,指定按哪一层进行转置,level参数的值可以是层号(整数),也可以是层的名称:
import numpy as np
import pandas as pd
data = pd.DataFrame(np.arange(6).reshape((2, 3)),
index=pd.Index(["Ohio", "Colorado"], name="state"),
columns=pd.Index(["one", "two", "three"],
name="number"))
# 使用 stack 方法将data的列转置到行中,从而生成一个 Series
result = data.stack()
print(result.unstack(level=0))
print(result.unstack(level="state"))
print(result.unstack(level=0)) 和 print(result.unstack(level="state")) 有相同的输出:
state | Ohio | Colorado |
---|---|---|
number | ||
one | 0 | 3 |
two | 1 | 4 |
three | 2 | 5 |
对于分层索引数据,如果在每个子组中未找到该分层级别中的所有值,则会引入缺失数据。再看下面的代码示例:
import numpy as np
import pandas as pd
s1 = pd.Series([0, 1, 2, 3], index=["a", "b", "c", "d"], dtype="Int64")
s2 = pd.Series([4, 5, 6], index=["c", "d", "e"], dtype="Int64")
data2 = pd.concat([s1, s2], keys=["one", "two"])
print(data2)
print(data2.unstack())
data2输出:
one a 0
b 1
c 2
d 3
two c 4
d 5
e 6
dtype: Int64
data2.unstack()输出(每个子组中未找到该级别中所有的值,引入了<NA>):
a | b | c | d | e | |
---|---|---|---|---|---|
one | 0 | 1 | 2 | 3 | <NA> |
two | <NA> | <NA> | 4 | 5 | 6 |
可以再通过data2.unstack().stack() 进行堆叠转换,重新转换为了data2,但如果在stack()方法种传入了future_stack=True参数,data2.unstack().stack(future_stack=True)输出:
one a 0
b 1
c 2
d 3
e <NA>
two a <NA>
b <NA>
c 4
d 5
e 6
dtype: Int64
因为我用的是pandas 2.1的版本索引使用的是它的新特性future_stack=True,如果是之前版本的pandas,可以用data2.unstack().stack(dropna=False)。
当我们在 DataFrame 中执行unstack操作(取消堆叠)时,用于执行操作的level级别将成为结果中的最低级别,如下代码示例:
import numpy as np
import pandas as pd
data = pd.DataFrame(np.arange(6).reshape((2, 3)),
index=pd.Index(["Ohio", "Colorado"], name="state"),
columns=pd.Index(["one", "two", "three"], name="number"))
# 使用 stack 方法将data的列转置到行中,从而生成一个 Series
result = data.stack()
df = pd.DataFrame({"left": result, "right": result + 5},
columns=pd.Index(["left", "right"], name="side"))
print(df)
print(df.unstack(level="state"))
df输出:
side | left | right | |
---|---|---|---|
state | number | ||
Ohio | one | 0 | 5 |
two | 1 | 6 | |
three | 2 | 7 | |
Colorado | one | 3 | 8 |
two | 4 | 9 | |
three | 5 | 10 |
df.unstack(level="state")输出:
side | left | right | ||
---|---|---|---|---|
state | Ohio | Colorado | Ohio | Colorado |
number | ||||
one | 0 | 3 | 5 | 8 |
two | 1 | 4 | 6 | 9 |
three | 2 | 5 | 7 | 10 |
与 unstack 操作一样,当调用 stack 时,我们可以指定要 stack 的轴的名称,例如:
import numpy as np
import pandas as pd
data = pd.DataFrame(np.arange(6).reshape((2, 3)),
index=pd.Index(["Ohio", "Colorado"], name="state"),
columns=pd.Index(["one", "two", "three"], name="number"))
# 使用 stack 方法将data的列转置到行中,从而生成一个 Series
result = data.stack()
df = pd.DataFrame({"left": result, "right": result + 5},
columns=pd.Index(["left", "right"], name="side"))
print(df)
print(df.unstack(level="state"))
print(df.unstack(level="state").stack(level="side"))
df.unstack(level="state").stack(level="side") 输出:
state | Ohio | Colorado | |
---|---|---|---|
number | side | ||
one | left | 0 | 3 |
right | 5 | 8 | |
two | left | 1 | 4 |
right | 6 | 9 | |
three | left | 2 | 5 |
right | 7 | 10 |
二、将 “Long” 转换为 “Wide” 格式
在数据库和 CSV 文件中会用长格式或堆叠格式存储多个时间序列,在这种格式中,单个值由表中的单行表示,而不是每行多个值。以下是一个存储时间序列的macrodata.csv文件:
下面我们对这个csv文件中的数据进行整理,首先我们先用read_csv方法加载该文件,并读取前5行:
import numpy as np
import pandas as pd
data = pd.read_csv("examples/macrodata.csv")
data = data.loc[:, ["year", "quarter", "realgdp", "infl", "unemp"]]
print(data.head())
data.head()输出:
year | quarter | realgdp | infl | unemp | |
---|---|---|---|---|---|
0 | 1959 | 1 | 2710.349 | 0.00 | 5.8 |
1 | 1959 | 2 | 2778.801 | 2.34 | 5.1 |
2 | 1959 | 3 | 2775.488 | 2.74 | 5.3 |
3 | 1959 | 4 | 2785.204 | 0.27 | 5.6 |
4 | 1960 | 1 | 2847.699 | 2.31 | 5.2 |
下面,我使用 pandas.PeriodIndex.from_fields()方法组合 year 和 quarter 列,并将合并后的值(每个季度结束日期)设置为索引,如下:
import numpy as np
import pandas as pd
data = pd.read_csv("examples/macrodata.csv")
data = data.loc[:, ["year", "quarter", "realgdp", "infl", "unemp"]]
periods = pd.PeriodIndex.from_fields(year=data.pop("year"),
quarter=data.pop("quarter"))
periods.name = "date"
print(periods)
print(periods)输出:
PeriodIndex(['1959Q1', '1959Q2', '1959Q3', '1959Q4', '1960Q1', '1960Q2',
'1960Q3', '1960Q4', '1961Q1', '1961Q2',
...
'2007Q2', '2007Q3', '2007Q4', '2008Q1', '2008Q2', '2008Q3',
'2008Q4', '2009Q1', '2009Q2', '2009Q3'],
dtype='period[Q-DEC]', name='date', length=203)
以上代码通过pandas.PeriodIndex.from_fields()方法使用data的year和quarter列组合创建了索引,并设置索引名为date。在获取year列和quarter列时使用的是pop方法,那么会在data中将这两列删除。下面我们将索引对象通过to_timestamp()方法转换为时间类型数组,并将其赋值给data索引,然后输出data前5行:
import numpy as np
import pandas as pd
data = pd.read_csv("examples/macrodata.csv")
data = data.loc[:, ["year", "quarter", "realgdp", "infl", "unemp"]]
periods = pd.PeriodIndex.from_fields(year=data.pop("year"),
quarter=data.pop("quarter"))
periods.name = "date"
data.index = periods.to_timestamp("D")
print(data.head())
输出结果:
realgdp | infl | unemp | |
---|---|---|---|
date | |||
1959-01-01 | 2710.349 | 0.00 | 5.8 |
1959-04-01 | 2778.801 | 2.34 | 5.1 |
1959-07-01 | 2775.488 | 2.74 | 5.3 |
1959-10-01 | 2785.204 | 0.27 | 5.6 |
1960-01-01 | 2847.699 | 2.31 | 5.2 |
下面我们再基于data,选择列的子集,并为列索引指定名称 “item”,输出前5行:
import numpy as np
import pandas as pd
data = pd.read_csv("examples/macrodata.csv")
data = data.loc[:, ["year", "quarter", "realgdp", "infl", "unemp"]]
periods = pd.PeriodIndex.from_fields(year=data.pop("year"),
quarter=data.pop("quarter"))
periods.name = "date"
data.index = periods.to_timestamp("D")
data = data.reindex(columns=["realgdp", "infl", "unemp"])
data.columns.name = "item"
print(data.head())
输出结果:
item | realgdp | infl | unemp |
---|---|---|---|
date | |||
1959-01-01 | 2710.349 | 0.00 | 5.8 |
1959-04-01 | 2778.801 | 2.34 | 5.1 |
1959-07-01 | 2775.488 | 2.74 | 5.3 |
1959-10-01 | 2785.204 | 0.27 | 5.6 |
1960-01-01 | 2847.699 | 2.31 | 5.2 |
最后,使用 stack 重新reshape,使用 reset_index 将新的索引level转换为列,并将包含数据值的列命名为 “value”:
import numpy as np
import pandas as pd
data = pd.read_csv("examples/macrodata.csv")
data = data.loc[:, ["year", "quarter", "realgdp", "infl", "unemp"]]
periods = pd.PeriodIndex.from_fields(year=data.pop("year"),
quarter=data.pop("quarter"))
periods.name = "date"
data.index = periods.to_timestamp("D")
data = data.reindex(columns=["realgdp", "infl", "unemp"])
data.columns.name = "item"
long_data = (data.stack().reset_index().rename(columns={0: "value"}))
print(long_data.loc[:10])
输出结果:
date | item | value | |
---|---|---|---|
0 | 1959-01-01 | realgdp | 2710.349 |
1 | 1959-01-01 | infl | 0.000 |
2 | 1959-01-01 | unemp | 5.800 |
3 | 1959-04-01 | realgdp | 2778.801 |
4 | 1959-04-01 | infl | 2.340 |
5 | 1959-04-01 | unemp | 5.100 |
6 | 1959-07-01 | realgdp | 2775.488 |
7 | 1959-07-01 | infl | 2.740 |
8 | 1959-07-01 | unemp | 5.300 |
9 | 1959-10-01 | realgdp | 2785.204 |
这种多时间序列长格式中,表中的每一行都表示一个观测值。
数据通常以上面输出结果中的方式存储在关系 SQL 数据库中。在前面的示例中,date 和 item 通常是主键(关系数据库的说法)。在某些情况下,这种格式的数据可能更难处理,例如,我们希望 DataFrame 包含每个不同项目值的一列,并在 Date 列中按时间戳编制索引,那么,我们可以使用DataFrame 的 pivot 方法执行这种转换(又将上面的long_data转换回去了):
import numpy as np
import pandas as pd
data = pd.read_csv("examples/macrodata.csv")
data = data.loc[:, ["year", "quarter", "realgdp", "infl", "unemp"]]
periods = pd.PeriodIndex.from_fields(year=data.pop("year"),
quarter=data.pop("quarter"))
periods.name = "date"
data.index = periods.to_timestamp("D")
data = data.reindex(columns=["realgdp", "infl", "unemp"])
data.columns.name = "item"
long_data = (data.stack().reset_index().rename(columns={0: "value"}))
pivoted = long_data.pivot(index="date", columns="item", values="value")
print(pivoted.head())
输出:
item | infl | realgdp | unemp |
---|---|---|---|
date | |||
1959-01-01 | 0.00 | 2710.349 | 5.8 |
1959-04-01 | 2.34 | 2778.801 | 5.1 |
1959-07-01 | 2.74 | 2775.488 | 5.3 |
1959-10-01 | 0.27 | 2785.204 | 5.6 |
1960-01-01 | 2.31 | 2847.699 | 5.2 |
传递给pivot()方法的前两个值分别是要用作行和列索引的列,最后是用于填充 DataFrame 的可选值列。 如果有两个要同时重塑的值列,我们可以这么做,看以下代码示例:
import numpy as np
import pandas as pd
data = pd.read_csv("examples/macrodata.csv")
data = data.loc[:, ["year", "quarter", "realgdp", "infl", "unemp"]]
periods = pd.PeriodIndex.from_fields(year=data.pop("year"),
quarter=data.pop("quarter"))
periods.name = "date"
data.index = periods.to_timestamp("D")
data = data.reindex(columns=["realgdp", "infl", "unemp"])
data.columns.name = "item"
long_data = (data.stack().reset_index().rename(columns={0: "value"}))
# 给long_data增加一列值value2
long_data["value2"] = np.random.standard_normal(len(long_data))
print(long_data[:10])
# 通过省略最后一个参数value,可以获得具有分层列的 DataFrame
pivoted = long_data.pivot(index="date", columns="item")
print(pivoted.head())
# 按value索引层数据前5行
print(pivoted["value"].head())
long_data[:10] 输出:
date | item | value | value2 | |
---|---|---|---|---|
0 | 1959-01-01 | realgdp | 2710.349 | 0.802926 |
1 | 1959-01-01 | infl | 0.000 | 0.575721 |
2 | 1959-01-01 | unemp | 5.800 | 1.381918 |
3 | 1959-04-01 | realgdp | 2778.801 | 0.000992 |
4 | 1959-04-01 | infl | 2.340 | -0.143492 |
5 | 1959-04-01 | unemp | 5.100 | -0.206282 |
6 | 1959-07-01 | realgdp | 2775.488 | -0.222392 |
7 | 1959-07-01 | infl | 2.740 | -1.682403 |
8 | 1959-07-01 | unemp | 5.300 | 1.811659 |
9 | 1959-10-01 | realgdp | 2785.204 | -0.351305 |
pivoted.head()输出:
value | value2 | |||||
---|---|---|---|---|---|---|
item | infl | realgdp | unemp | infl | realgdp | unemp |
date | ||||||
1959-01-01 | 0.00 | 2710.349 | 5.8 | 0.575721 | 0.802926 | 1.381918 |
1959-04-01 | 2.34 | 2778.801 | 5.1 | -0.143492 | 0.000992 | -0.206282 |
1959-07-01 | 2.74 | 2775.488 | 5.3 | -1.682403 | -0.222392 | 1.811659 |
1959-10-01 | 0.27 | 2785.204 | 5.6 | 0.128317 | -0.351305 | -1.313554 |
1960-01-01 | 2.31 | 2847.699 | 5.2 | -0.615939 | 0.498327 | 0.174072 |
pivoted["value"].head()输出:
item | infl | realgdp | unemp |
---|---|---|---|
date | |||
1959-01-01 | 0.00 | 2710.349 | 5.8 |
1959-04-01 | 2.34 | 2778.801 | 5.1 |
1959-07-01 | 2.74 | 2775.488 | 5.3 |
1959-10-01 | 0.27 | 2785.204 | 5.6 |
1960-01-01 | 2.31 | 2847.699 | 5.2 |
注意:pivot 等效于使用 set_index 创建分层索引,然后调用 unstack:
pivoted = long_data.pivot(index="date", columns="item")
与
unstacked = long_data.set_index(["date", "item"]).unstack(level="item") 等效。
三、将 “Wide” 格式转换为 “Long” 格式
对 DataFrames 进行透视的反向操作是 pandas.melt。它不是在新的 DataFrame 中将一列转换为多列,而是将多列合并为一列,从而生成比输入更长的 DataFrame。看以下代码示例:
import numpy as np
import pandas as pd
df = pd.DataFrame({"key": ["foo", "bar", "baz"],
"A": [1, 2, 3],
"B": [4, 5, 6],
"C": [7, 8, 9]})
print(df)
melted = pd.melt(df, id_vars="key")
print(melted)
df输出:
key | A | B | C | |
---|---|---|---|---|
0 | foo | 1 | 4 | 7 |
1 | bar | 2 | 5 | 8 |
2 | baz | 3 | 6 | 9 |
melted输出:
key | variable | value | |
---|---|---|---|
0 | foo | A | 1 |
1 | bar | A | 2 |
2 | baz | A | 3 |
3 | foo | B | 4 |
4 | bar | B | 5 |
5 | baz | B | 6 |
6 | foo | C | 7 |
7 | bar | C | 8 |
8 | baz | C | 9 |
以上将“key” 列作为是组指示符,其他列是数据值。使用 pandas.melt 时,我们必须指出哪些列(如果有)是组指示符。使用 pivot,我们可以将形状重塑回原始布局,例如:
import numpy as np
import pandas as pd
df = pd.DataFrame({"key": ["foo", "bar", "baz"],
"A": [1, 2, 3],
"B": [4, 5, 6],
"C": [7, 8, 9]})
print(df)
melted = pd.melt(df, id_vars="key")
print(melted)
reshaped = melted.pivot(index="key", columns="variable", values="value")
print(reshaped)
reshaped输出:
variable | A | B | C |
---|---|---|---|
key | |||
bar | 2 | 5 | 8 |
baz | 3 | 6 | 9 |
foo | 1 | 4 | 7 |
由于 pivot 的结果从用作行标签的列创建索引,因此我们可以使用 reset_index 将数据移回列:
import numpy as np
import pandas as pd
df = pd.DataFrame({"key": ["foo", "bar", "baz"],
"A": [1, 2, 3],
"B": [4, 5, 6],
"C": [7, 8, 9]})
print(df)
melted = pd.melt(df, id_vars="key")
reshaped = melted.pivot(index="key", columns="variable", values="value")
reshaped.reset_index()
print(reshaped)
reshaped.reset_index() 后输出:
variable | key | A | B | C |
---|---|---|---|---|
0 | bar | 2 | 5 | 8 |
1 | baz | 3 | 6 | 9 |
2 | foo | 1 | 4 | 7 |
上面在使用melt()方法时,除了指示符列key外,默认将其他列都作为了值列,我们还可以指定要用作值列的列子集:
pd.melt(df, id_vars="key", value_vars=["A", "B"])
这行代码输出只有A,B列作为值列的结果:
key | variable | value | |
---|---|---|---|
0 | foo | A | 1 |
1 | bar | A | 2 |
2 | baz | A | 3 |
3 | foo | B | 4 |
4 | bar | B | 5 |
5 | baz | B | 6 |
pandas.melt 也可以在没有任何组标识符的情况下使用,例如:
import numpy as np
import pandas as pd
df = pd.DataFrame({"key": ["foo", "bar", "baz"],
"A": [1, 2, 3],
"B": [4, 5, 6],
"C": [7, 8, 9]})
a = pd.melt(df, value_vars=["A", "B", "C"])
b = pd.melt(df, value_vars=["key", "A", "B"])
以上a输出:
variable | value | |
---|---|---|
0 | A | 1 |
1 | A | 2 |
2 | A | 3 |
3 | B | 4 |
4 | B | 5 |
5 | B | 6 |
6 | C | 7 |
7 | C | 8 |
8 | C | 9 |
b输出:
variable | value | |
---|---|---|
0 | key | foo |
1 | key | bar |
2 | key | baz |
3 | A | 1 |
4 | A | 2 |
5 | A | 3 |
6 | B | 4 |
7 | B | 5 |
8 | B | 6 |
总结:通过以上的学习,我掌握了一些用于数据导入、清理和重组的 pandas 基础知识,后面继续学习使用 matplotlib 进行数据可视化以及pandas更深入的应用。