稀にではあるのですが、Pandasのデータ(DataFrame/Series)のデータの順位を取得したくなることがあります。
これまでは、DataFrameの列内の順位であれば、sort_valuesで並べ替えて、インデックスを振り直して、といった手順で対応することが多かったです。しかし、この方法では、値が等しい項目の扱いが少々厄介になります。また、最近、列内の順位ではなく、各行ごとに行内での順位を取得したいことがあり、ちょっと面倒だなと感じることがありました。
そこで、改めて調べてみたのですが、DataFrameもSeriesもそれぞれ、rankというメソッドを持っていて、これを使えば簡単に順位が取得できることがわかりました。
参考:
pandas.DataFrame.rank
pandas.Series.rank
使い方非常に簡単で、rank()を呼び出すだけです。適当なDataFrameでやってみます。
import pandas as pd
# 適当にデータを生成する
df = pd.DataFrame(
{
"col1": [20, 30, None, 20, 10, 20],
"col2": [10, 50, 20, 20, 30, 60],
"col3": [30, None, 60, None, 20, 80]
}
)
print(df)
"""
col1 col2 col3
0 20.0 10 30.0
1 30.0 50 NaN
2 NaN 20 60.0
3 20.0 20 NaN
4 10.0 30 20.0
5 20.0 60 80.0
"""
# 列内の順位を取得する
print(df.rank())
"""
col1 col2 col3
0 3.0 1.0 2.0
1 5.0 5.0 NaN
2 NaN 2.5 3.0
3 3.0 2.5 NaN
4 1.0 4.0 1.0
5 3.0 6.0 4.0
"""
結果を見てわかる通り、順序は昇順で、値が小さいほど高順位(数値が小さい)ですね。
さて、このrank()メソッドはとても気が利いていて、多くの引数で細かく結果を制御できます。
まず、列ごとではなく、行ごとの順位が欲しい場合は、axis引数に1を渡します。
ちなみに、Seriesの方のドキュメントにも、axis引数があって、1を渡せるような記載があるのですがこれはおそらくドキュメントの誤りです。(普通にエラーになります。)
# 行内の順位を取得する
print(df.rank(axis=1))
"""
col1 col2 col3
0 2.0 1.0 3.0
1 1.0 2.0 NaN
2 NaN 1.0 2.0
3 1.5 1.5 NaN
4 1.0 3.0 2.0
5 1.0 2.0 3.0
"""
昇順ではなく降順の順位が欲しい、という場合は、ascending にFalse を渡します。(デフォルトはTrueです。)
# 降順の順位を取得する
print(df.rank(ascending=False))
"""
col1 col2 col3
0 3.0 6.0 3.0
1 1.0 2.0 NaN
2 NaN 4.5 2.0
3 3.0 4.5 NaN
4 5.0 3.0 4.0
5 3.0 1.0 1.0
"""
na_option という引数で、NaN値に対応する順位を指定できます。
“keep”(デフォルト) であれば、NaNのままです。
“top”にすると、最も高い順位(要するに1)がNaN値に振り分けられます。
“bottom”にすると、逆にもっとも低い順位が割り振られます。
それぞれ実行した結果が以下です。
df = pd.DataFrame(
{"data": [20, 30, None, 20, 10, 20]}
)
df["na_keep"] = df.data.rank(na_option="keep")
df["na_top"] = df.data.rank(na_option="top")
df["na_bottom"] = df.data.rank(na_option="bottom")
print(df)
"""
data na_keep na_top na_bottom
0 20.0 3.0 4.0 3.0
1 30.0 5.0 6.0 5.0
2 NaN NaN 1.0 6.0
3 20.0 3.0 4.0 3.0
4 10.0 1.0 2.0 1.0
5 20.0 3.0 4.0 3.0
"""
さて、最初の方のコードの実行例で、2.5など小数点の順位のものがあるのがわかると思います。これは同率順位の項目に対して、デフォルトではその平均順位を返す設定になっているからです。
この設定は、 method 引数で制御できます。値はデフォルトの’average’の他、最小値(もっとも高順位)を採用する’min’、その逆に最大値を採用する’max’、元の配列に表示されていた順に順位がつく’first’、’min’と同じように、最小値が採用されるが、その次の順位の項目の順位が数が飛ばないように採番される’dense’の5種類の値が指定できます。
ちょっとわかりにくいと思うので実例でやってみます。
df = pd.DataFrame(
{"data": [20, 30, 40, 20, 10, 20, 40]}
)
df["m_average"] = df.data.rank(method="average")
df["m_min"] = df.data.rank(method="min")
df["m_max"] = df.data.rank(method="max")
df["m_first"] = df.data.rank(method="first")
df["m_dense"] = df.data.rank(method="dense")
print(df)
"""
data m_average m_min m_max m_first m_dense
0 20 3.0 2.0 4.0 2.0 2.0
1 30 5.0 5.0 5.0 5.0 3.0
2 40 6.5 6.0 7.0 6.0 4.0
3 20 3.0 2.0 4.0 3.0 2.0
4 10 1.0 1.0 1.0 1.0 1.0
5 20 3.0 2.0 4.0 4.0 2.0
6 40 6.5 6.0 7.0 7.0 4.0
"""
値が20の項目が3つあって順位的には、2位,3位,4位に相当するのですが、
averageであれば3、minであれば2、maxであれば4が割り振られているのが確認できましたね。firstであれば元の配列に出てきた通り、2,3,4位が当てられています。
そして、denseの結果を見ると、minと同様に20は2位になっているのですが、その次の30が、minの時は5位だったのに、denseでは欠番がなくこれが3位になっています。
あとは、あまり使わないと思うのですが、 pct という引数をTrueにすると、順位の数値ではなくパーセンタイルで結果が受け取れます。
df = pd.DataFrame(
{"data": [20, 30, 10, 20, 40]}
)
df["pct_false"] = df.data.rank(pct=False)
df["pct_true"] = df.data.rank(pct=True)
print(df)
"""
data pct_false pct_true
0 20 2.5 0.5
1 30 4.0 0.8
2 10 1.0 0.2
3 20 2.5 0.5
4 40 5.0 1.0
"""
順位が一番低い項目が1になるのは想像通りですが、最高順位の項目は0では無いんですね。