60664


### 実行環境

  * pandas 0.25.3
  * Python 3.7.4

### やりたいこと

pandasのDataFramdeを使って、下図のような表をCSVとして出力したいです。

[![画像の説明をここに入力](https://i.stack.imgur.com/Ayr05.png)](https://i.stack.imgur.com/Ayr05.png)

### 試したこと

以下のPythonコードを実行しました。

[code]

        dict_table = {("user1", "plan"): [1, 11], 
             ("user1", "work"): [2, 12],
             ("user2", "plan"): [3, 13],
             ("user2", "work"): [4, 14]
             }
        df = pandas.DataFrame(dict_table)
        df.to_csv("foo.csv", index=False)
    
[/code]

出力結果(foo.csv)

[code]

    user1,user1,user2,user2
    plan,work,plan,work
    1,2,3,4
    11,12,13,14
    
[/code]

### 期待していた結果

本当は以下のように、ヘッダには同じユーザを一度しか表示しないフォーマットで出力したいです。

[code]

    user1,,user2,
    plan,work,plan,work
    1,2,3,4
    11,12,13,14
    
[/code]

### 質問

pandasを使って、上記のようなCSVは出力する方法を教えていただきたいです。


CC BY-SA 4.0
2019-11-19T10:40:28.480
60659
2019-11-22T01:06:16.417
2019-11-20T01:39:02.097
19524
19524
question
python pandas
pandasで、複数列に対応するヘッダをCSVで出力したいです。


ヘッダは`csv`で、データは`pandas`の追記モード指定で、別々に書くという考え方があります。

決め打ちで作るならファイルへの書き出し部分をこちらで：

[code]

    import csv
    csvfilename = 'foo.csv'
    with open(csvfilename, 'w', newline='') as f:
        writer = csv.writer(f)
        # ヘッダ部を決め打ちで書き出し
        writer.writerows([['user1','','user2',''],['plan','work','plan','work']])
    
    # データ部のみ追記
    df.to_csv(csvfilename, index=False, mode='a', header=False)
    
[/code]

DataFrameから動的に作る場合はこちらで：

[code]

    import pandas
    import csv
    
    dict_table = {("user1", "plan"): [1, 11], 
         ("user1", "work"): [2, 12],
         ("user2", "plan"): [3, 13],
         ("user2", "work"): [4, 14]
         }
    df = pandas.DataFrame(dict_table)
    
    # DataFrameのheader部分を抽出してリスト化
    dfhead = [list(x) for x in list(df.columns.values)]
    lines = [len(v) for v in dfhead]
    hdwork = []
    for i in range(lines[0]):
        hdwork.append(sum([a[i:(i+1)] for a in dfhead], []))
    
    # 各行内で直前と同じ文字列なら空文字列に変更
    header = []
    for row in hdwork:
        prev = ''
        rowwork = []
        for item in row:
            if item != prev:
                rowwork.append(item)
                prev = item
            else:
                rowwork.append('')
    
        header.append(rowwork)
    
    # ヘッダ部書き出し
    csvfilename = 'foo.csv'
    with open(csvfilename, 'w', newline='') as f:
        writer = csv.writer(f)
        writer.writerows(header)
    
    # データ部のみ追記
    df.to_csv(csvfilename, index=False, mode='a', header=False)
    
[/code]

* * *

**追記**  
要求事項とは違いますが、いったん作った複数列データを使い回したいのでセーブしておきたい、読み込みも簡単にしたいと考えれば、[`to_excel()`](https://pandas.pydata.org/pandas-
docs/stable/reference/api/pandas.DataFrame.to_excel.html) /
[`read_excel()`](https://pandas.pydata.org/pandas-
docs/stable/reference/api/pandas.read_excel.html) が使えるかもしれません。  
ただし出来るexcelファイルは癖があってコレジャナイ感が強いですが。

使うのは単純に1行：

[code]

    df.to_excel('foo.xlsx', header=[0,1])
    
[/code]

出来たexcelファイルはこれなので、非常に違和感があります。  
[![to_excel\(\)結果](https://i.stack.imgur.com/eSTtd.png)](https://i.stack.imgur.com/eSTtd.png)

pandasドキュメントでは`index`を無くせるように見えますが、未実装あるいは想定していない使い方のようで`index=False`を指定するとエラーになります。  
またヘッダ部とデータ部の間に空行が1行入ります。

しかし読み込みはこちらの1行で出来て、DataFrameの再現は簡単です。

[code]

    df2 = pd.read_excel('foo.xlsx', header=[0,1], index_col=0)
    
[/code]

読み込んで出来たDataFrameは以下になります。

[code]

    >>> df2
      user1      user2
       plan work  plan work
    0     1    2     3    4
    1    11   12    13   14
    
[/code]

* * *

csvにこだわるならまた別の方法として、上記`to_excel()`/`read_excel()`に使われてそうな`openpyxl`の[`dataframe_to_rows()`](https://openpyxl.readthedocs.io/en/stable/api/openpyxl.utils.dataframe.html)と`csv`を組み合わせるという手も考えられます。  
こちらは`index`を消せるのですが、ヘッダ部とデータ部間の空行は残るのでちょっとした工夫が必要です。

[code]

    from openpyxl.utils.dataframe import dataframe_to_rows
    import csv
    
    rows = dataframe_to_rows(df, index=False) # index無し指定が有効
    csvframe = []
    for r_idx, row in enumerate(rows, 1):
        csvrow = []
        for c_idx, value in enumerate(row, 1):
            csvrow.append(value)
    
        if len(csvrow):        # 有効データのある行だけリストに追加
            csvframe.append(csvrow)
    
    with open('foo.csv', 'w', newline='') as f:
        writer = csv.writer(f)
        writer.writerows(csvframe)
    
[/code]


CC BY-SA 4.0
2019-11-19T12:55:06.860
60662
2019-11-22T01:06:16.417
2019-11-22T01:06:16.417
26370
26370
60659
answer


`levels` に空文字列(`''`)をセットした MultiIndex オブジェクトを作成して、df のインデックスにセットします。

[code]

    >>> midx = pd.MultiIndex(
      levels = [
        list(v) + [''] for v in df.columns.levels
      ],
      codes = [
        [
          y if n == 0 else
            (len(df.columns.levels[i]) if y == (list(v))[n-1] else y)
          for n, y in enumerate(list(v))
        ]
        for i, v in enumerate(df.columns.codes)
      ]
    )
    
    >>> midx
    MultiIndex(levels=[['user1', 'user2', ''], ['plan', 'work', '']],
               codes=[[0, 2, 1, 2], [0, 1, 0, 1]])
    
    >>> df.T.set_index(midx).T.to_csv('output.csv', index=False)
    
[/code]

**output.csv**

[code]

    user1,,user2,
    plan,work,plan,work
    1,2,3,4
    11,12,13,14
    
[/code]

**追記**

対象データはカラム数が少ないので、MultiIndex オブジェクトを手で書き下しても良いかと思います。

[code]

    >>> df.columns
    MultiIndex(levels=[['user1', 'user2'], ['plan', 'work']],
               codes=[[0, 0, 1, 1], [0, 1, 0, 1]])
    
[/code]

`df.columns.codes` で、同じインデックス番号が続く部分を変更します。以下のコードにおける、変更されている値 `2` は
`levels[0][2]`(空文字) に対応しています。

[code]

    >>> df.T.set_index(
          pd.MultiIndex(
            levels = [['user1', 'user2', ''], ['plan', 'work']],
             codes = [[0, 2, 1, 2], [0, 1, 0, 1]]
        )).T.to_csv('output.csv', index=False)
    
[/code]

**追記(2019/11/21)**

> to_csvメソッドなどのオプションなどでは対応できないのですね。

無理をすればできなくはないですが、お勧めはしません。

[code]

    import csv
    
    output = StringIO()
    df.to_csv(output,
      index      = False,
      header     = ['user1', '', 'user2', '\nplan,work,plan,work'],
      quoting    = csv.QUOTE_NONE,
      escapechar = '\01')
    
    with open('output.csv', 'w', encoding='utf-8') as f:
      f.write(output.getvalue().replace('\01', ''))
    
[/code]

`csv.QUOTE_NONE`
を設定していますので、数値データのみの場合は問題ないのですが、`,`(カンマ/セパレータ)が含まれる文字列データの場合にはクォート処理が行われませんので、CSV
としては不正なデータになってしまいます。


CC BY-SA 4.0
2019-11-19T15:42:04.183
60664
2019-11-21T10:21:11.470
2019-11-21T10:21:11.470
60659
answer