最近、とあるデータを分析する前のデータの品質チェックで、ある値の列が単調増加になっているかどうかを判定する必要が発生しました。全グループ単調増加のはずのデータだったのに、そうなっていないのが見つかったため、一通りチェックすることにしたのです。
Pandasのデータだったので、普通に差分をとって全部正であることを見ればよかったのですが、もっとスマートな方法がないか探してみたところ実際に良い方法が見つかったのでその紹介です。ちなみに差分の取り方は過去に記事にしています。
参考: Dataframeの差分を取る
さて、今回見つけたのは、PandasのSeries(とindexオブジェクトも)に用意されている、以下のプロパティです。(3個目のやつは1個目のエイリアスなので実質2個ですが)
– pandas.Series.is_monotonic
– pandas.Series.is_monotonic_decreasing
– pandas.Series.is_monotonic_increasing (is_monotonicのエイリアス)
is_monotonic と is_monotonic_increasing はそのSeriesが単調増加ならTrue、そうでないならFalse、is_monotonic_decreasing はそのSeriesが単調現象ならTrue、そうでないならばFalseを返してくれます。
いちいち差分取って確認したりしなくても、最初からフラグを持っていたというとてもありがたい話でした。
注意しないといけなのは、count()やsum()などのメソッドではなくプロパティなので使う時にカッコは要らないのと、当然引数が渡せないので細かな調整などはできず用意された仕様でそのまま使うしかないということでしょうか。(メソッドにしていただいて、狭義/広義やNoneの扱いなどが調整できるともっと便利だったのですが。)
とりあえず使ってみましょう。サンプルは全部 is_monotonic (単調増加)でやってみます。
単調減少の場合は、is_monotonic_decreasingをつかうだけで基本的には同じです。
まず普通に単調増加の時とそうでないときで、True/ Falseが変わっているのをみます。
下の例で分かる通り、判定は広義の単調増加の場合であってもTrueです。
import pandas as pd
# (広義)単調増加の場合
sr = pd.Series([1, 1, 2, 3, 5])
print(sr.is_monotonic)
# True
# (狭義)単調増加の場合
sr = pd.Series([1, 2, 4, 9, 16])
print(sr.is_monotonic)
# True
# 単調増加ではない場合
sr = pd.Series([1, -2, 3, -4, 5])
print(sr.is_monotonic)
# False
Noneなどが入ってる場合はそれ以外の部分が単調増加でもFalseになるようです。
sr = pd.Series([None, 1, 2, 3, 5])
print(sr.is_monotonic)
# False
値が数値ではなく文字列の場合も使えます。大小が定義できるものなら良いようです。
sr = pd.Series(["abc", "lmn", "xyz"])
print(sr.is_monotonic)
# True
sr = pd.Series(["z", "y", "x"])
print(sr.is_monotonic)
# False
広義ではなく狭義単調増加の判定がしたいんだということもあると思うのですが、上の方でも書いた通り、is_monotonicはプロパティなのでそう言った細かい調整はできません。どうやったらスマートに判定できるのかなと、考えたのですが、まず is_monotonic で単調増加性を判定した後に、値のユニークカウントと値の数を比較するのがいいのではないかと思いました。狭義単調増加であれば全て異なる値になっているはずだからです。
# (広義)単調増加の場合
sr = pd.Series([1, 1, 2, 3, 5])
print(sr.is_monotonic)
# True
# 重複する値があるのでFalseになる
print(sr.nunique() == sr.count())
# False
# (狭義)単調増加の場合
sr = pd.Series([1, 2, 4, 9, 16])
print(sr.is_monotonic)
# True
# 重複する値がないのでTrueになる
print(sr.nunique() == sr.count())
# True
2022/07/05 追記
このユニークの判定について、コメントで教えていたのですが、is_unique というそのものズバリな属性が存在していました。nunique()とcount() を比較するより、以下のようにis_unique見た方がずっとスマートです。
# (広義)単調増加の場合
sr = pd.Series([1, 1, 2, 3, 5])
print(sr.is_monotonic)
# True
# 重複する値があるのでFalseになる
print(sr.is_unique)
# False
さて、ここまで Seriesについて書いて来ましたが、DataFrameのデータについて調べたい場合もあります。というか、単一のSeriesについて調べるのであれば差分取って調べてもそんなに手間ではなく、僕が今回この方法を調査してたのはデータフレームで大量のデータについて調査する必要があったからです。
データの持ち方について、普通のパターンがありうるのでまずは列別にその列全体が単調増加かどうかを調べる方法を紹介します。サンプルデータはこちらです。
df = pd.DataFrame(
{
"year": [2010, 2011, 2012, 2013, 2014],
"col1": [1, 1, 2, 3, 5],
"col2": [1, 2, 4, 9, 16],
"col3": [1, -2, 3, -4, 5],
}
)
print(df)
"""
year col1 col2 col3
0 2010 1 1 1
1 2011 1 2 -2
2 2012 2 4 3
3 2013 3 9 -4
4 2014 5 16 5
"""
PandasのDataFrameにはis_monotonicなどのプロパティはありません。df.is_monotonicとかしても、各列について一括で調査したりはできないようです。
(これがメソッドであるsum()やcount()との大きな違いですね。)
for文を回して列ごとにis_monotonicをやってもいいのですが、 applyを使うのがスマートではないでしょうか。以下のようにして、col3だけ単調増加ではないことがすぐわかりました。
print(df.apply(lambda col: col.is_monotonic))
"""
year True
col1 True
col2 True
col3 False
dtype: bool
"""
次に、別のデータの持ち方をしているDataFrameを見てみましょう。これは、値は1列に全部入ってるのですが、カテゴリーを示す列があり、カテゴリーごとに単調増加かどうかを判定したいというケースです。こういうやつです。
df = pd.DataFrame(
{
"type": ["type1"] * 5 + ["type2"] * 5 + ["type3"]*5,
"year": [2010, 2011, 2012, 2013, 2014] * 3,
"value": [1, 1, 2, 3, 5] + [1, 2, 4, 9, 16] + [1, -2, 3, -4, 5],
}
)
print(df)
"""
type year value
0 type1 2010 1
1 type1 2011 1
2 type1 2012 2
3 type1 2013 3
4 type1 2014 5
5 type2 2010 1
6 type2 2011 2
7 type2 2012 4
8 type2 2013 9
9 type2 2014 16
10 type3 2010 1
11 type3 2011 -2
12 type3 2012 3
13 type3 2013 -4
14 type3 2014 5
"""
これもtypeが3種類くらいで、値も全部で15個みたいな上のサンプルのようなやつならfor文で回してもそんなに大変ではないですが、データが莫大になるとスマートな方法が欲しくなります。いろいろ考えたのですが、素直にgroupbyで分割して、applyでlambda式を当てていくのが良いと思います。何度も書いていますが、メソッドではないので、groupby(“type”).is_monotonic() のような書き方では動きません。
print(df.groupby("type").apply(lambda x: x.value.is_monotonic))
"""
type
type1 True
type2 True
type3 False
dtype: bool
"""
以上が、is_monotonic の使い方や応用例の紹介になります。単調増加とか単調減少の判定をしたいって場面は多くないかもしれませんが、いざ必要になるとこれらのプロパティは非常に便利で、元のデータがリストやNumpyのArrayだった場合はこれのためにわざわざSeriesに変換してもいいのではないかと思うレベルです。機会があればこれらの存在を思い出してください。
2022/07/05 追記
これもコメントで教えていただきましたが、lambdaを使わない書き方ができます。Groupbyした後に、列名を角括弧(ブラケット)で指定すると、is_monotonic_decreasing やis_monotonic_increasing が使えます。SeriesGroupBy オブジェクトが量プロパティを持っていたようです。 (なぜか is_monotonic は無いのでエラーになります。)
print(df.groupby("type")["value"].is_monotonic_increasing)
"""
type
type1 True
type2 True
type3 False
Name: value, dtype: bool
"""
print(df.groupby("type")["value"].is_monotonic_decreasing)
"""
type
type1 False
type2 False
type3 False
Name: value, dtype: bool
"""
# is_monotonic は動かない。
try:
print(df.groupby("type")["value"].is_monotonic)
except Exception as e:
print(e)
# 'SeriesGroupBy' object has no attribute 'is_monotonic'
`sr.nunique() == sr.count()`は`sr.is_unique`で取得できます。
また最後の例は「groupby(“type”).is_monotonic() のような書き方では動きません。」とありますが、`df.groupby(“type”)[“value”].is_monotonic_increasing`は可能です。
教えていただきありがとうございます。
それぞれブログ記事に反映させていただきました。
自分のコードがあまり綺麗に書けてないと思っていた部分だったので改善できて嬉しいです。