pandasにはDataFrameオブジェクトの行や列をソートする機能や、行と列の順序を入れ替える機能もあります。今回はそれらについて見ていきましょう。
本シリーズ「Pythonデータ処理入門」は、Pythonの基礎をマスターした人を対象に以下のような、Pythonを使ってデータを処理しようというときに便利に使えるツールやライブラリ、フレームワークの使い方の基礎を説明するものです。
なお、本連載では以下のバージョンを使用しています。
第6回ではDataFrameオブジェクトに対して行や列を追加したり、削除したりする方法を紹介しました。今回はDataFrameオブジェクトをソートしたり、行や列を入れ替えたりする方法を説明しましょう。
DataFrameオブジェクトをソートするには次の2つのメソッドを使います。
sort_indexメソッドは行ラベルまたは列ラベルの昇順(降順)で行または列を並べ替えると考えてください。これに対して、DataFrameオブジェクトの指定した行または列の要素を基準にソートするのがsort_valuesメソッドです。以下でこれらの使い方を見ていきましょう。
pandas.DataFrame.sort_index(*, axis=0, ascending=True, inplace=False,
na_position='last', ignore_index=False, key=None)
axisパラメーターで指定した軸に沿ってラベルを基準に行または列をソートする。パラメーターは以下の通り(抜粋)。全てのパラメーターはキーワード引数形式で指定する必要がある点に注意されたい(省略可能)。全パラメーターについてはpandasのドキュメント「pandas.DataFrame.sort_index」を参照のこと。
以下では次のようなDataFrameオブジェクトをソートしてみます。行インデックスが降順になっている('row4'から'row0')ことに注意してください。
somedata = [['foo', 25, 1.5], ['bar', 30, 2.7], ['baz', 35, 3.2],
['qux', 28, 1.9], ['quux', 32, 2.1]]
df = pd.DataFrame(somedata, columns=['Name', 'Age', 'Value'],
index=[f'row{n}' for n in range(4, -1, -1)])
print(df)
# 出力結果:
# Name Age Value
#row4 foo 25 1.5
#row3 bar 30 2.7
#row2 baz 35 3.2
#row1 qux 28 1.9
#row0 quux 32 2.1
まずは簡単な例から見てみましょう。
df_sorted = df.sort_index(axis=0) # 行方向にソート:df.sort_index()と同じ
print(df_sorted)
# 出力結果:
# Name Age Value
#row0 quux 32 2.1
#row1 qux 28 1.9
#row2 baz 35 3.2
#row3 bar 30 2.7
#row4 foo 25 1.5
df_sorted = df.sort_index(axis=1) # 列方向にソート
print(df_sorted)
# 出力結果:
# Age Name Value
#row4 25 foo 1.5
#row3 30 bar 2.7
#row2 35 baz 3.2
#row1 28 qux 1.9
#row0 32 quux 2.1
最初の例ではaxisパラメーターに0を指定しているのでDataFrameオブジェクトが行ラベル基準でソートされます(axisパラメーターの指定を省略したのと同様)。降順に振った行ラベル(と行)が昇順に並んでいる点に注目してください。
次の例では1を指定しているのでDataFrameオブジェクトが列ラベル基準でソートされます。Name→Age→Valueだった列の並びがAge→Name→Valueの順に変わっている点に注目してください。
次にascendingパラメーターを指定する例です。
df_sorted = df.sort_index(axis=1, ascending=False) # 列方向にソート(降順)
print(df_sorted)
# 出力結果:
# Value Name Age
#row4 1.5 foo 25
#row3 2.7 bar 30
#row2 3.2 baz 35
#row1 1.9 qux 28
#row0 2.1 quux 32
この例では、axisパラメーターに1を、ascendingパラメーターにFalseを指定しています。そのため、列がValue→Name→Ageと降順にソートされました。
inplaceパラメーターにTrueを指定すると、元のDataFrameオブジェクトの内容が変更されます。このとき、戻り値はありません。以下に例を示します。
result = df.sort_index(axis=0, inplace=True) # 行方向にソートして、元のDataFrameを更新
print(type(result)) # <class 'NoneType'>
print(df)
# 出力結果:
# Name Age Value
#row0 quux 32 2.1
#row1 qux 28 1.9
#row2 baz 35 3.2
#row3 bar 30 2.7
#row4 foo 25 1.5
ignore_indexパラメーターをTrueにすると、ソートされた軸のインデックスが0始まりの整数インデックスとして振り直されます。
df_sorted = df.sort_index(axis=1, ignore_index=True)
print(df_sorted)
# 出力結果:
# 0 1 2
#row0 32 quux 2.1
#row1 28 qux 1.9
#row2 35 baz 3.2
#row3 30 bar 2.7
#row4 25 foo 1.5
keyパラメーターにはソートの際にラベルに適用する関数やメソッドを指定できます。以下の例ではラムダ式を与えて、その中でpandas.Series.str.lenメソッドを呼び出すことで、ラベルの文字数を基準にソートしています。
df_sorted = df.sort_index(axis=1, key=lambda x: x.str.len()) # 列ラベルの文字数でソート
print(df_sorted)
# 出力結果:
# Age Name Value
#row0 32 quux 2.1
#row1 28 qux 1.9
#row2 35 baz 3.2
#row3 30 bar 2.7
#row4 25 foo 1.5
大文字小文字を無視してソートしたいのであれば、「lambda x: x.str.upper()」か「lambda x: x.str.lower()」などとすればよいでしょう。
最後にna_positionパラメーターについて見てみます。これはDataFrameオブジェクトのラベルにNaN値が含まれていた場合に、その行または列を先頭か('first'を指定)末尾('last'を指定)のどちらに置くかを指定するものです。
nan = float('nan')
df.columns = ['Name', 'Age', nan] # 列ラベルにNaN値を含める
df_sorted = df.sort_index(axis=1, na_position='first') # NaN値を先頭に置く
print(df_sorted)
# 出力結果:
# NaN Age Name
#row0 2.1 32 quux
#row1 1.9 28 qux
#row2 3.2 35 baz
#row3 2.7 30 bar
#row4 1.5 25 foo
この例では「na_position='first'」としているので、ラベルがNaN値になっている列が先頭に置かれています。
ここまで見てきたように、sort_indexメソッドは行ラベルまたは列ラベルを基準として行や列の順序を入れ替えるものです。次に見るsort_valuesメソッドは指定した行または列の要素の値を基準にDataFrameオブジェクトをソートするのに使います。
pandas.DataFrame.sort_values(by, *, axis=0, ascending=True, inplace=False,
na_position='last', ignore_index=False, key=None)
axisパラメーターで指定した軸に沿って、byパラメーターで指定した行または列を基準にDataFrameオブジェクトをソートする。パラメーターは以下の通り(抜粋)。全パラメーターについてはpandasのドキュメント「pandas.DataFrame.sort_values」を参照のこと。
byパラメーターは位置引数として指定可能だが、他のパラメーターは全てキーワード引数の形式で指定する必要がある(省略可能)。
ここでは先ほどのDataFrameオブジェクトから少しその内容を変更したものを作成して、例として使用します。
somedata = [['foo', 25, 3.5], ['bar', 30, 2.7], ['baz', 25, 3.2],
['qux', 28, 1.9], ['quux', 32, 2.1]]
df = pd.DataFrame(somedata, columns=['Name', 'Age', 'Value'],
index=[f'row{n}' for n in range(4, -1, -1)])
print(df)
# 出力結果:
# Name Age Value
#row4 foo 25 3.5
#row3 bar 30 2.7
#row2 baz 25 3.2
#row1 qux 28 1.9
#row0 quux 32 2.1
'Age'列に同じ値(25)の要素がある点に注目しておきましょう。では、その列をbyパラメーターに指定してソートしてみます。
df_sorted = df.sort_values(by='Age') # Age列で昇順にソート
print(df_sorted)
# 出力結果:
# Name Age Value
#row4 foo 25 3.5
#row2 baz 25 3.2
#row1 qux 28 1.9
#row3 bar 30 2.7
#row0 quux 32 2.1
byパラメーターに'Age'を渡しているので、ここでは'Age'列の値を基準にソートしています。ここで'Age'列の値が同じ(25)行について'Value'列を見ると、その値は降順になっています(3.5と3.2)。'Age'列は昇順に並んでいるので、ここも昇順の方が気持ちが良い(または都合が良い)のであれば、byパラメーターに「by=['Age', 'Value']」のようにソートに使用する列のラベルを要素とするリストを渡します。すると、以下のように両方の列について昇順にソートできるようになります。
df_sorted = df.sort_values(by=['Age', 'Value'])
print(df_sorted)
# 出力結果:
# Name Age Value
#row2 baz 25 3.2
#row4 foo 25 3.5
#row1 qux 28 1.9
#row3 bar 30 2.7
#row0 quux 32 2.1
なお、axis=1を指定すると、列方向のソートになりますが、上で見たDataFrameオブジェクトでは行の要素は文字列、整数、浮動小数点数で構成されています。これらを比較するのは無理です(意味がありません)。pandasでは多くの場合、特定の列が特定のカテゴリの値を含み、行はそれらをまとめた1件のデータのように取り扱うことが多いと思われますが、特定の行が特定のカテゴリの値を含み、列がそれらをまとめた1件のデータのような形式のデータも考えられます。ここではDataFrameオブジェクトの行と列を入れ替えたものを考えてみましょう。
行と列を入れ替えた(転置した)ものはpandas.DataFrame.T属性で得られます。
pandas.DataFrame.T
元のDataFrameオブジェクトを行と列を入れ替えた(転置した)もの。以下に例を示す。
tmp = pd.DataFrame([[1, 2], [3, 4]], columns=['c0', 'c1'], index=['r0', 'r1'])
print(tmp)
# 出力結果:
# c0 c1
#r0 1 2
#r1 3 4
print(tmp.T)
# 出力結果:
# r0 r1
#c0 1 3
#c1 2 4
元のDataFrameオブジェクトの行と列を入れ替えてみましょう(入れ替えた後に列名も変更しています)。
df_t = df.T
df_t.columns = [f'col{n}' for n in range(4, -1, -1)]
print(df_t)
# 出力結果:
# col4 col3 col2 col1 col0
#Name foo bar baz qux quux
#Age 25 30 25 28 32
#Value 3.5 2.7 3.2 1.9 2.1
この形式のデータであれば、列方向にソートできます。例えば、'Name'行をbyパラメーターに指定して、列方向にソートするなら次のようになります。
df_sorted = df_t.sort_values(by='Name', axis=1)
print(df_sorted)
# 出力結果:
# col3 col2 col4 col0 col1
#Name bar baz foo quux qux
#Age 30 25 25 32 28
#Value 2.7 3.2 3.5 2.1 1.9
ソートするときには、自分がどんな形式のデータを扱っていて、それをどの軸に沿ってソートするのが適切かには注意するようにしましょう。
他のパラメーターについては、既に見ているので説明は省略します。
pandas.DataFrame.reindexメソッドはDataFrameオブジェクトが、指定したインデックス(行ラベルや列ラベル)を持つようにします。このときに、行や列の入れ替えや、新規の行や列の追加、行や列の削除などが発生します。
pandas.DataFrame.reindex(labels=None, *, index=None, columns=None, axis=None,
method=None, fill_value=nan, limit=None)
指定したインデックスに合致するように、元のDataFrameオブジェクトを変形した新しいDataFrameオブジェクトを返す。パラメーターは以下の通り(抜粋)。全パラメーターについてはpandasのドキュメント「pandas.DataFrame.reindex」を参照のこと。
ここでは以下のようなDataFrameオブジェクトを例としましょう。
somedata = [[0, 2, 4, 6], [10, 12, 14, 16], [20, 22, 24, 26]]
df = pd.DataFrame(somedata, columns=[f'col{n}' for n in range(0, 8, 2)],
index=[n for n in range(0, 30, 10)])
print(df)
# 出力結果:
# col0 col2 col4 col6
#0 0 2 4 6
#10 10 12 14 16
#20 20 22 24 26
まずはlabelsパラメーターとaxisパラメーターを指定して、行または列を入れ替える方法を見てみます。
df_reindexed = df.reindex([20, 10, 0])
print(df_reindexed)
# 出力結果:
# col0 col2 col4 col6
#20 20 22 24 26
#10 10 12 14 16
#0 0 2 4 6
df_reindexed = df.reindex(labels=['col6', 'col2', 'col4', 'col0'], axis=1)
print(df_reindexed)
# 出力結果:
# col6 col2 col4 col0
#0 6 2 4 0
#10 16 12 14 10
#20 26 22 24 20
最初の例では行を入れ替えています。labelsパラメーターにはキーワード引数形式ではなく位置引数として新しいラベルを(リストの形で)渡し、axisパラメーターの指定は省略しています。このときには、行ラベルが渡されたものとして処理されます。新しいラベルは「[20, 10, 0]」なので、先頭行と末尾行が入れ替わっていることに注目してください。
次の例ではキーワード形式でlabelsパラメーターに列ラベルのリストを渡し、axisパラメーターには1を渡しています。そのため、labelsパラメーターは新しい列ラベルとして解釈され、その結果、先頭列と末尾列が入れ替わりました。
あるいはindexパラメーターやcolumnsパラメーターを指定することで、軸を明示して新しいインデックスを渡すことも可能です。以下に例を示します。
df_reindexed = df.reindex(index=[20, 10, 0])
print(df_reindexed) # 出力結果は上と同様
df_reindexed = df.reindex(columns=['col6', 'col2', 'col4', 'col0'])
print(df_reindexed) # 出力結果は上と同様
両者を指定することももちろんできます。
df_reindexed = df.reindex(index=[20, 10, 0],
columns=['col6', 'col2', 'col4', 'col0'])
print(df_reindexed)
# 出力結果:
# col6 col2 col4 col0
#20 26 22 24 20
#10 16 12 14 10
#0 6 2 4 0
ここで新しいラベルとして一部のラベルだけを要素とするリストを渡してみると対のようになります。
df_reindexed = df.reindex(columns=['col0', 'col6', 'col4'])
print(df_reindexed)
# 出力結果:
# col0 col6 col4
#0 0 6 4
#10 10 16 14
#20 20 26 24
この例では、「columns=['col0', 'col6', 'col4']」としています。そのため、'col2'列が削除され、'col6'列と'col4'列とが入れ替わったDataFrameオブジェクトが得られました。
では、元のDataFrameオブジェクトには存在しない行インデックスを指定するとどうなるかを見てみましょう(既に行や列を追加と書いているので予想は付くでしょう)。
df_reindexed = df.reindex(index=[0, 5, 10, 15, 20])
print(df_reindexed)
# 出力結果:
# col0 col2 col4 col6
#0 0.0 2.0 4.0 6.0
#5 NaN NaN NaN NaN
#10 10.0 12.0 14.0 16.0
#15 NaN NaN NaN NaN
#20 20.0 22.0 24.0 26.0
このように存在しない行インデックスに対応する行が追加されました。その値はNaN値です。NaN値ではなく、特定の値としたいときにはfill_valueパラメーターにその値を指定します。
df_reindexed = df.reindex(index=[0, 5, 10, 15, 20], fill_value=float('inf'))
print(df_reindexed)
# 出力結果:
# col0 col2 col4 col6
#0 0.0 2.0 4.0 6.0
#5 inf inf inf inf
#10 10.0 12.0 14.0 16.0
#15 inf inf inf inf
#20 20.0 22.0 24.0 26.0
ここでは「fill_value=float('inf')」としているので、今度はNaN値ではなく追加された行の要素の値はinfになりました。
methodパラメーターを使う方法もあります。methodパラメーターには以下の値を指定できます。なお、methodパラメーターが正しく機能するのは行インデックスまたは列インデックスが単調増加または単調減少している場合だけであることには注意してください(文字列のラベルを与えている場合には正しく機能しません)。
それぞれについて見てみましょう。
# 'pad'/'ffill'は直前の値を使用する
df_reindexed = df.reindex(index=[0, 5, 10, 15, 20], method='ffill')
print(df_reindexed)
# 出力結果:
# col0 col2 col4 col6
#0 0 2 4 6
#5 0 2 4 6
#10 10 12 14 16
#15 10 12 14 16
#20 20 22 24 26
# 'backfill'/'bfill'は直後の値を使用する
df_reindexed = df.reindex(index=[0, 5, 10, 15, 20, 25], method='bfill')
print(df_reindexed)
# 出力結果:
# col0 col2 col4 col6
#0 0.0 2.0 4.0 6.0
#5 10.0 12.0 14.0 16.0
#10 10.0 12.0 14.0 16.0
#15 20.0 22.0 24.0 26.0
#20 20.0 22.0 24.0 26.0
#25 NaN NaN NaN NaN
# 'nearest'は直後の値を使用する
df_reindexed = df.reindex(index=[0, 7, 10, 12, 20], method='nearest')
print(df_reindexed)
# 出力結果:
# col0 col2 col4 col6
#0 0 2 4 6
#7 10 12 14 16
#10 10 12 14 16
#12 10 12 14 16
#20 20 22 24 26
最初の例ではmethodパラメーターに'ffill'を指定しているので、追加された行の要素の値には直前の要素の値が使われています。次の例ではmethodパラメーターに'bfill'を指定しているので、追加された行の要素の値には次の要素の値が使われています。行インデックス「25」の要素の値がNaN値になっているのは、直後の要素がないからです。そして、最後の例ではmethodパラメーターに'nearest'を指定しているので、行インデックスが最も近い要素の値が使われています。
このときには最大で連続何個の要素の値をmethodパラメーターに指定した方法で決定するかどうかを指定できます。以下に例を示します。
df_reindexed = df.reindex(index=[0, 2, 4, 6, 8, 10, 20],
method='bfill', limit=3)
print(df_reindexed)
# 出力結果:
# col0 col2 col4 col6
#0 0.0 2.0 4.0 6.0
#2 NaN NaN NaN NaN # limitを超えたので'bfill'されない
#4 10.0 12.0 14.0 16.0 # limit以内なので'bfill'される
#6 10.0 12.0 14.0 16.0 # limit以内なので'bfill'される
#8 10.0 12.0 14.0 16.0 # limit以内なので'bfill'される
#10 10.0 12.0 14.0 16.0
#20 20.0 22.0 24.0 26.0
この例では、「method='bfill'」なので直後の要素の値が使われます。また、「limit=3」なので最大で連続3個の要素の値がこの方法で決まります。そのため、行インデックス10の上にある行インデックス「8」「6」「4」についてはそれらの要素の値は「10.0」と直後の要素(行インデックス10)の値が使われていますが、行インデックス「4」は上限の3個を超えたのでその値が使われなかったということです。
こうした自動的な値の決定(穴埋め)が適切かどうかはそのときどきに応じて変わるでしょうから、適切な方法を選択するようにしてください。
今回はDataFrameオブジェクトをソートする方法と、行や列の入れ替えと追加や削除の方法を紹介しました。次回はデータ型の変換や、pandasのDataFrameオブジェクトとNumPyの多次元配列の相互変換などについて触れる予定です。
Copyright© Digital Advantage Corp. All Rights Reserved.