環境
- Python 3.12.1
- pandas 2.2.0
やりたいこと
pandas.pivot_tableで集計したpandas.DataFrameを、CSVファイルに出力したいです。
空のDataFrameを集計した場合は、CSVファイルにはヘッダ行のみ出力したいです。そうすることで、どのようなDataFrameでもCSVファイルの構造(列情報)は保たれるからです。
import pandas as pd
def aggregate(df: pd.DataFrame) -> pd.DataFrame:
return df.pivot_table(index="name", aggfunc="sum", values="working_hours")
df1 = pd.DataFrame(
{"name": ["A", "A", "B"], "class": ["X", "Y", "Z"], "working_hours": [1, 2, 4]}
)
aggregate(df1).to_csv("df1.csv")
# 空のDataFrameを生成
df2 = pd.DataFrame(columns=["name", "class", "working_hours"])
# df1のdtypesと同じにした
df2 = df2.astype({"working_hours": "int64"})
aggregate(df2).to_csv("df2.csv")
しかし、空のDataFrameを集計した結果をCSVファイルに出力すると、name
しか出力されませんでした。
$ python sample.py
$ cat df1.csv
name,working_hours
A,3
B,4
$ cat df2.csv
name
DataFrameの中身を一つずつ確認する
生成されたDataFrameの中身を一つずつ確認していきます。
空でないDataFrame
集計前のDataFrame
In [208]: df1 = pd.DataFrame(
...: {"name": ["A", "A", "B"], "class": ["X", "Y", "Z"], "working_hours": [1, 2, 4]}
...: )
In [209]: df1
Out[209]:
name class working_hours
0 A X 1
1 A Y 2
2 B Z 4
In [210]: df1.dtypes
Out[210]:
name object
class object
working_hours int64
dtype: object
集計後のDataFrame
In [218]: df1_agg = df1.pivot_table(index="name", aggfunc="sum", values="working_hours")
In [219]: df1_agg
Out[219]:
working_hours
name
A 3
B 4
In [220]: df1_agg.dtypes
Out[220]:
working_hours int64
dtype: object
In [221]: df1_agg.columns
Out[221]: Index(['working_hours'], dtype='object')
In [222]: df1_agg.index
Out[222]: Index(['A', 'B'], dtype='object', name='name')
空のDataFrame
集計前のDataFrame
In [229]: df2 = pd.DataFrame(columns=["name", "class", "working_hours"])
In [246]: df2=df2.astype({"working_hours":"float"})
In [247]: df2
Out[247]:
Empty DataFrame
Columns: [name, class, working_hours]
Index: []
In [248]: df2.dtypes
Out[248]:
name object
class object
working_hours float64
dtype: object
集計後のDataFrame
In [249]: df2_agg = df2.pivot_table(index="name", aggfunc="sum", values="working_hours")
In [250]: df2_agg
Out[250]:
Empty DataFrame
Columns: []
Index: []
In [251]: df2_agg.dtypes
Out[251]: Series([], dtype: object)
In [252]: df2_agg.columns
Out[252]: Index([], dtype='object')
In [253]: df2_agg.index
Out[253]: Index([], dtype='object', name='name')
分かったこと
集計後のDataFrameのcolumns
プロパティ
空でないDataFramedf1
をpivot_tableで集計すると、columns
プロパティは['working_hours']
でした。それに対して、空のDataFramedf2
を集計すると、columns
プロパティは[]
で空でした。
df1
とdf2
のcolumns
プロパティは同じ値なので、pivot_table
で集計した結果のcolumns
プロパティも同じ値になることを、私は期待していました。なぜ同じ値にならないのでしょうか?
直接的な原因は分かりませんでした。
ただ、pivot_table
にcolumns="class"
引数を指定すると、集計したDataFrameのcolumns
はclass
列の値['X', 'Y', 'Z']
になります。
In [375]: df1_agg1 = df1.pivot_table(index="name", values="working_hours", columns="class", aggfunc="sum",fill_value=0)
In [377]: df1_agg1
Out[377]:
class X Y Z
name
A 1 2 0
B 0 0 4
In [378]: df1_agg1.columns
Out[378]: Index(['X', 'Y', 'Z'], dtype='object', name='class')
pivot_table
で集計したDataFrameのcolumns
は、集計前のDataFrameの値によって変わるので、空のDataFrameをpivot_table
で集計したDataFrameのcolumns
プロパティは空なのかもしれません。
集計後のDataFrameのindex
プロパティ
pivot_table
でindex
引数に渡した列名name
が、index
プロパティのname
に格納されていました。このname
プロパティの値が、CSVファイルdf2.csv
に出力されていました。
対応方法
空のDataFrameをpivot_table
で集計しない
aggregate()
に渡されたDataFrameが空の場合は、個別に空のDataFrameを返すようにしました。
def aggregate(df: pd.DataFrame) -> pd.DataFrame:
if len(df) == 0:
df_empty = pd.DataFrame(columns=["working_hours"], index=pd.Index([],name="name"))
return df_empty.astype({"working_hours": "int64"})
return df.pivot_table(index="name", aggfunc="sum", values="working_hours")
$ python sample.py
$ cat df2.csv
name,working_hours
In [242]: df2_agg2 = df2.pivot_table(index="name", aggfunc="sum", values="working_hours", columns="class", fill_value=0)
In [243]: df2_agg2
Out[243]:
Empty DataFrame
Columns: []
Index: []
In [244]: df2_agg2.dtypes
Out[244]: Series([], dtype: object)
In [245]: df2_agg2.columns
Out[245]: Index([], dtype='object', name='class')
pivot_table
でなくgroupby
で集計する
今回の例だとpivot_table
のcolumns
プロパティを使用していないので、groupby()
でも同じ結果を得ることができました。
def aggregate(df: pd.DataFrame) -> pd.DataFrame:
return df.groupby("name")[["working_hours"]].sum()
In [380]: df1.groupby("name")[["working_hours"]].sum()
Out[380]:
working_hours
name
A 3
B 4
In [383]: df2_agg3 = df2.groupby("name")[["working_hours"]].sum()
In [384]: df2_agg3.columns
Out[384]: Index(['working_hours'], dtype='object')
補足:空のDataFrameに対してできること
私の知る範囲では、空でないDataFrameに対して実行した結果のcolumns
プロパティは、空のDataFrameに対して実行した結果と同じでした。
以下に、実際に実行した結果を記載します。
merge
In [387]: df3 = pd.DataFrame({"name": ["A", "C", "C"], "country": ["Japan", "U.S.", "U.K."]})
In [388]: df3
Out[388]:
name country
0 A Japan
1 C U.S.
2 C U.K.
In [393]: df3.merge(df2, on="name", how="left")
Out[393]:
name country class working_hours
0 A Japan NaN NaN
1 C U.S. NaN NaN
2 C U.K. NaN NaN
列の追加
# どんな値を設定してもよい
In [398]: df2["category"] = None
In [399]: df2
Out[399]:
Empty DataFrame
Columns: [name, class, working_hours, working_hours*2, category]
Index: []
concat
In [401]: pandas.concat([df1,df2])
Out[401]:
name class working_hours
0 A X 1
1 A Y 2
2 B Z 4