YutaKaのPython教室

Python の文法やライブラリ、API、環境構築について画像・動画・ソースコード付きで徹底解説!

pandasで見るデータの縦持ち・横持ち|stack, unstack

データを集計する際は、データ構造が縦持ちか横持ちかで処理のしやすさが大きく変わってきます。

この記事では、データの縦持ち・横持ちについて、次のような内容を紹介してきます。

  1. そもそも縦持ち・横持ちの違いやメリット・デメリットは?
  2. 具体的に縦持ち・横持ちってどうやって使えばいいの?
  3. pythonのデータ分析ライブラリpandasで縦持ち・横持ちを変換する方法は?

縦持ち・横持ちを意識して扱えれば、データ集計が一気に楽になると思います!

縦持ち・横持ちのデータとは?

そもそもデータの「縦持ち」や「横持ち」とは、何のことをいっているのでしょうか?

大まかなイメージは、次の通りです。

  • 「縦持ち」、「横持ち」は、データ構造の分類の方法
    • 縦持ち:ラベルが縦に並んだ、縦に長いデータ
    • 横持ち:ラベルが横にも並んだ、横に長いデータ

サンプルデータで、縦持ち、横持ちのデータを比較して見ましょう

横持ちデータはExcelの集計表などでよく見る形ですね。

一方、縦持ちデータは縦に長くなっています。

縦持ち・横持ちデータのメリット・デメリット

次に縦持ち、横持ちデータのメリット・デメリットを整理してみましょう。

データ構造 メリット デメリット
横持ちデータ ● 人が見やすい
● 列同士の比較ができる
● データの追加がしにくい
● 欠損値がでやすい
縦持ちデータ ● プログラムで使いやすい(集計しやすい)
● データの追加がしやすい
● 人が見て見にくい
● 行数が多くなりがち

特にデータ分析で重要な違いは、データの追加のしやすさです。

サンプルデータに、「2023年の東京」のデータを追加してみます。

縦持ちではデータ構造は変化しませんが、横持ではデータ構造が変化してしまいます。

データ管理の観点では、データを追加してもデータ構造が変化しない縦持ちの方が便利です。

各持ち方のメリット、デメリットを参考にしっかり使い分けましょう!

pandasで縦持ち・横持ちの変換

Pythonのデータ分析ライブラリpandasでは、どのように縦持ち・横持ちを変換するのでしょうか?

実はpandasには、縦持ち・横持ちを変換する便利なメソッドが用意されています。

  • 横持ち→縦持ち:.stack()メソッド
  • 縦持ち→横持ち:.unstack()メソッド

stackは、「積み上げる」という意味なので、横から縦に積み上げると覚えれば、覚えやすいですね。

逆にunstackは、積み上げたものをよこに並べると覚えれば、縦→横の変換だと覚えやすいです。

以下では、各メソッドの使い方を詳しく解説していきます。

横持ち→縦持ち|stack()

横持ちのデータを縦持ちに変換する場合は、.stack()メソッドを使用します。

.stack()メソッドは、引数で次の内容を設定することが可能です。

設定内容 引数 コメント
欠損値の除去 dropna=True or False デフォルト:True
縦にするカラムのレベル level = カラムレベル デフォルト:-1(一番内側のカラム)
※マルチインデックスの場合のみ指定

マルチインデックスでは、levelの指定が必要な場合もありますが、通常はデフォルト設定で問題ないことが多いです。

まずは、マルチインデックスでない次のサンプルデータで横持ち→縦持ちの変換をしてみます。

import pandas as pd
df = pd.DataFrame([[100, 90, 120],
                   [80,  70,  80], 
                   [50,  60,  80],
                   [70,  80, None]],
                  index=pd.Index(["Tokyo", "Chiba", "Saitama", "Kanagawa"], name="Shope"),
                  columns=pd.Index(["2020", "2021", "2022"], name="Year"))
print(df)
# Year      2020  2021   2022
# Shope                      
# Tokyo      100    90  120.0
# Chiba       80    70   80.0
# Saitama     50    60   80.0
# Kanagawa    70    80    NaN  # <-あえて欠損値を準備

このデータを縦持ちに変換してみましょう。

s = df.stack()

print(s)
# Shope     Year
# Tokyo     2020    100.0
#           2021     90.0
#           2022    120.0
# Chiba     2020     80.0
#           2021     70.0
#           2022     80.0
# Saitama   2020     50.0
#           2021     60.0
#           2022     80.0
# Kanagawa  2020     70.0
#           2021     80.0  # 欠損値は除去された。
# dtype: float64

無事、縦持ちのデータが生成されました。

変換後の列数が一つの場合は、Seriesが返されます。

デフォルトでは、dropna=Trueなので欠損値は除去されています。

また、Seriesの名前が設定されていないので、必要に応じて.rename()と組み合わせましょう。

s = df.stack().rename("Sales")

print(s)
# Shope     Year
# Tokyo     2020    100.0
#           2021     90.0
#           2022    120.0
# Chiba     2020     80.0
#           2021     70.0
#           2022     80.0
# Saitama   2020     50.0
#           2021     60.0
#           2022     80.0
# Kanagawa  2020     70.0
#           2021     80.0
# Name: Sales, dtype: float64  # <-Nameが設定されている
マルチインデックスの場合

カラムがマルチインデックスの場合は、level引数で縦持ちにするレベルを単独または複数指定できます。

  • level = カラムレベル番号 or カラム名
    • カラムレベル → 外側から内側に向かって、0, 1,…の連番
    • 一番内側は、-1でも指定可能
  • level = [カラムレベル番号 or カラム名のリスト]

カラムレベルのイメージ図は次の通りです。

次のマルチインデックスのサンプルデータで.stack()の挙動を確認してみましょう。

multi_col = pd.MultiIndex.from_product([["April", "May", "June"], ["Apple", "Banana"]],
                                       names=["Month", "Product"])
index = pd.Index(["Tokyo", "Chiba", "Saitama"], name="Shop")

df = pd.DataFrame([[10, 10, 20, 20, 40, 30],
                   [10, 20, 40, 30, 20, 40],
                   [40, 10, 20, 40, 30, 10]],
                  columns=multi_col, index=index)
print(df)
# Month   April          May         June       
# Product Apple Banana Apple Banana Apple Banana
# Shop                                          
# Tokyo      10     10    20     20    40     30
# Chiba      10     20    40     30    20     40
# Saitama    40     10    20     40    30     10

まずはデフォルト設定(level=-1)で、縦持ちにしてみます。

df_stacked = df.stack()
 
print(df_stacked)
# Month            April  June  May
# Shop    Product                  
# Tokyo   Apple       10    40   20
#         Banana      10    30   20
# Chiba   Apple       10    20   40
#         Banana      20    40   30
# Saitama Apple       40    30   20
#         Banana      10    10   40

一番内側の"Product"が、縦方向に変換されました。

変換後も複数列が残っているため、出力はSereisでなく、DataFrameです。

次にlevel=0を指定して、"Month"だけを縦方向に変換してみましょう、

df_stacked = df.stack(level=0)
 
print(df_stacked)
# Product        Apple  Banana
# Shop    Month               
# Tokyo   April     10      10
#         June      40      30
#         May       20      20
# Chiba   April     10      20
#         June      20      40
#         May       40      30
# Saitama April     40      10
#         June      30      10
#         May       20      40

今度は、"Product"カラムが残されて、"Month"カラムが縦に移動しました。

最後にlevel=[0,1]で全てのカラムを縦に移動してみましょう。

s_stacked = df.stack(level=[0,1])
 
print(s_stacked)
# Shop     Month  Product
# Tokyo    April  Apple      10
#                 Banana     10
#          June   Apple      40
#                 Banana     30
#          May    Apple      20
#                 Banana     20
# Chiba    April  Apple      10
#                 Banana     20
#          June   Apple      20
#                 Banana     40
#          May    Apple      40
#                 Banana     30
# Saitama  April  Apple      40
#                 Banana     10
#          June   Apple      30
#                 Banana     10
#          May    Apple      20
#                 Banana     40

カラムが全て縦方向に移動しましたね。

データ列が一つだけなので、出力もSeriesになっています。

縦持ち→横持ち|unstack()

縦持ちのデータを横持ちに変換する場合は、.unstack()メソッドを使用します。

.unstack()メソッドは、引数で次の内容を設定することが可能です。

設定内容 引数 コメント
欠損値の除去 fill_value=数値 or 文字列 欠損値を埋める値
※指定しないと欠損値のまま
横にするインデックスのレベル level = インデックスレベル デフォルト:-1(一番内側のインデックスカラム)

シングルインデックスでは横に移動する列がないので、縦→横の変換はマルチインデックスでのみ可能です。

.stack()の解説で生成した、次のマルチインデックスのデータを使用して挙動を確認してみましょう。

s_stacked

# Shop     Month  Product
# Tokyo    April  Apple      10
#                 Banana     10
#          June   Apple      40
#                 Banana     30
#          May    Apple      20
#                 Banana     20
# Chiba    April  Apple      10
#                 Banana     20
#          June   Apple      20
#                 Banana     40
#          May    Apple      40
#                 Banana     30
# Saitama  April  Apple      40
#                 Banana     10
#          June   Apple      30
#                 Banana     10
#          May    Apple      20
#                 Banana     40

まずはデフォルト設定(level=-1)で、縦持ちにしてみます。

df_unstacked = s_stacked.unstack()
 
print(df_unstacked)
# Product        Apple  Banana
# Shop    Month               
# Chiba   April     10      20
#         June      20      40
#         May       40      30
# Saitama April     40      10
#         June      30      10
#         May       20      40
# Tokyo   April     10      10
#         June      40      30
#         May       20      20

一番内側の"Product"が、横方向に変換されました。

縦持ちから横持ちにすると、データが見やすいことがよくわかります。

次にlevel=1を指定して、"Month"だけを横方向に変換してみましょう、

df_unstacked = s_stacked.unstack(level=1)
 
print(df_unstacked)
# Month            April  June  May
# Shop    Product                  
# Chiba   Apple       10    20   40
#         Banana      20    40   30
# Saitama Apple       40    30   20
#         Banana      10    10   40
# Tokyo   Apple       10    40   20
#         Banana      10    30   20

今度は、"Month"が横に移動しました。

月ごとの集計を見たい場合には、この横持ちデータも見やすいですね。

最後にlevel=[1, 2]で、"Month""Product"を横に移動してみます。

df_unstacked = s_stacked.unstack(level=[1,2])
 
print(df_unstacked)
# Month   April         June          May       
# Product Apple Banana Apple Banana Apple Banana
# Shop                                          
# Chiba      10     20    20     40    40     30
# Saitama    40     10    30     10    20     40
# Tokyo      10     10    40     30    20     20

店舗ごとの集計を見たい場合には、この横持ちデータが良さそうです。

このように.unstack()を使用すると、縦持ちから横持ちにして、人が見やすい形式に変換できます。

最後に、縦持ち・横持ちを使用したデータ集計例を紹介します。

縦持ち・横持ち変換を使用したデータ集計方法

縦持ち・横持ちの持ち方を変えて、データ集計する方法を紹介します。

  1. データ処理しやすいように、横持ち→縦持ちに変換

  2. .groupby()でデータを集計

  3. 結果を見やすくするため、縦持ち→横持ちに変換

データが縦持ちになっていると、カテゴリごとのデータ集計がとても簡単だとわかると思います。

次のサンプルデータで、縦持ち・横持ち変換例を見てみましょう

import pandas as pd
multi_col = pd.MultiIndex.from_product([["April", "May", "June"], ["Apple", "Banana"]],
                                       names=["Month", "Product"])
index = pd.Index(["Tokyo", "Chiba", "Saitama"], name="Shop")

df = pd.DataFrame([[10, 10, 20, 20, 40, 30],
                   [10, 20, 40, 30, 20, 40],
                   [40, 10, 20, 40, 30, 10]],
                  columns=multi_col, index=index)
print(df)
# Month   April          May         June       
# Product Apple Banana Apple Banana Apple Banana
# Shop                                          
# Tokyo      10     10    20     20    40     30
# Chiba      10     20    40     30    20     40
# Saitama    40     10    20     40    30     10

横持ちデータのままだと、"Apple""Banana"の平均や合計を集計するのが少し大変です。

まずは、集計しやすいように、データを縦持ちに変換します。

s = df.stack([0,1])
print(s)
# Shop     Month  Product
# Tokyo    April  Apple      10
#                 Banana     10
#          June   Apple      40
#                 Banana     30
#          May    Apple      20
#                 Banana     20
# Chiba    April  Apple      10
#                 Banana     20
#          June   Apple      20
#                 Banana     40
#          May    Apple      40
#                 Banana     30
# Saitama  April  Apple      40
#                 Banana     10
#          June   Apple      30
#                 Banana     10
#          May    Apple      20
#                 Banana     40

各月の果物の合計を.groupby()で集計してみましょう。

.groupby()は、複数のインデックス列やデータ列を指定できるので縦持ちデータと非常に相性がいいです。

s_agg = s.groupby(["Month", "Product"]).sum()
print(s_agg)
 
# Month  Product
# April  Apple      60
#        Banana     40
# June   Apple      90
#        Banana     80
# May    Apple      80
#        Banana     90
# dtype: int64

簡単に集計できましたが、縦持ちのままでは結果が見にくいですね。

最後に横持ちデータに変換して、見やすくしておきましょう。

df_res = s_agg.unstack(level=0)

print(df_res)
# Month    April  June  May
# Product                  
# Apple       60    90   80
# Banana      40    80   90

今回の例では、次のようにデータ構造を順次変化させています。

  • 横持ち→縦持ち→(集計)→横持ち
  • データ集計のときは縦持ち
  • 結果を確認するときは横持ち

目的に応じて適切なデータ構造に変化させると、データの集計がとてもスムーズです。

縦持ちデータの集計で使用した.groupby()については、次の記事で詳しく解説しています。

縦持ちデータの集計に欠かせないメソッドの一つなので、是非チェックしてください。

≫pandas groupbyでグループ化|図解でわかりやすく解説
pandasの.groupby()を使うと、DataFrameの要素をもとにデータをグループ分けして、簡単に集計することができます。①そもそもどうやって.groupbyで、グループ分けするの?②グループ分け結果の確認方法は?③具体的にどうやってグループごとの集計するの?こんな悩みを図解・サンプルコート付きでわかりやすく解決します!
www.yutaka-note.com/entry/pandas_groupby
 

オススメ|pandasとデータ分析の勉強方法

今回は、縦持ち・横持ちデータの特徴、pandasでの変換方法について解説しました。

pandasは便利すぎて操作方法がわかりにくいことがよくあります…。

結局はコツコツ学ぶのが、pandasマスターの近道ですよね!

≫【ブログカテゴリー:pandas】

データ分析初心者の方にはこちらの記事もおススメです。

私がこれまで勉強してきた経験をもとに考えたおススメの勉強本の紹介記事です。

何から始めて、どうやってレベルアップしていけばいいのか、初心者の方にぜひおススメしたい本を紹介しました。

≫独学でデータ分析を勉強するオススメ学習本
独学でのpythonデータ分析勉強に役立ったおススメ書籍を紹介していきます。業務でそれなりにデータ分析を行えるまで、いろいろな試行錯誤をしてきましたが、もし自分が今ゼロから勉強する立場ならどうするのがいいのか考えてみました。以下では、入門書、個別モジュール用、実践用の3つの視点でおススメ本を紹介していきます。
www.yutaka-note.com/entry/data_analysis
 

オススメのpandas本については、次の記事で紹介しています。

≫【レビュー】「Python実践データ分析100本ノック」|100本終えたらpandasが好きになっていた
Python実践データ分析100本ノックで、実際に100本終了したレビューです。pythonでのデータ分析の入門書としてかなりの良書だったと思います。・python2~3冊目に何を勉強しようか迷っている人・時間をかけずにデータ分析の基本を学びたい人・pandasへの抵抗を減らしたい人
www.yutaka-note.com/entry/nock_100
 
≫【レビュー】「Pythonによるデータ分析入門」| pandas開発者によるpandasユーザーのためのpandasの教科書!
「Pythonによるデータ分析入門」を、最初から最後まで実際に実践してみたレビューです。具体的にどのようなことができるようになったかを実例付きで紹介します!・DataFrameの生成方法・欠損値の処理方法・グラフ化の方法気になる学習時間は…?
www.yutaka-note.com/entry/2019/12/07/230219