空のDataFrameに対して`pivot_table`を実施すると、`columns`プロパティが空のDataFrameが生成される

# 環境
* Python 3.12.1
* pandas 2.2.0


# やりたいこと
[pandas.pivot_table](https://pandas.pydata.org/docs/reference/api/pandas.pivot_table.html)で集計したpandas.DataFrameを、CSVファイルに出力したいです。
空のDataFrameを集計した場合は、CSVファイルにはヘッダ行のみ出力したいです。そうすることで、どのようなDataFrameでもCSVファイルの構造（列情報）は保たれるからです。

```python:sample.py
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`プロパティ
空でないDataFrame`df1`をpivot_tableで集計すると、`columns`プロパティは`['working_hours']`でした。それに対して、空のDataFrame`df2`を集計すると、`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を返すようにしました。

```python
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()`でも同じ結果を得ることができました。

```python
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
```