luggage baggage

Machine learning, data analysis, web technologies and things around me.

pandas.Series を使うときには index に注意しよう

Python でデータ分析をする際、pandas.DataFrame を基本的な道具として使うことは多いと思います。また、DataFrame の各カラムは pandas.Series で自然に表現されており、新しいカラムを Series として書いた上で、DataFrame に追加することもあるでしょう(DataFrame の適当なカラムに対して rolling 関数を適用して移動平均を計算した際に返ってくる Series を追加する、とか)。この時、意図した順にデータを追加するためには、index に注意しておく必要があります。

例として、

df = pd.DataFrame(dict(a=range(3)))
#    a
# 0  0
# 1  1
# 2  2

df.index
# RangeIndex(start=0, stop=3, step=1)

という DataFrame があるとします。この時、index は左側に書かれている "0 1 2" です(実装としては RangeIndex)。

さて、ここに Series で表される新しいデータをカラムとして追加したいとしましょう。ただし、データ処理を事前にした都合上、index は 0 から始まらず、次のようになっているとします。

s = pd.Series(range(3), index=list(range(3))[::-1])
# 2    0
# 1    1
# 0    2
# dtype: int64

s.index
# Int64Index([2, 1, 0], dtype='int64'

つまり、"2 1 0" の順に上から並んでいるわけです。

ここで、s を安直に df の一カラムとして代入するとどうなるか?

df["s"] = s
df
#    a  s
# 0  0  2
# 1  1  1
# 2  2  0

s カラムを見ると、代入前の Series と順番が逆になってしまっていますね。

さらにわかりやすい例として、上と同じ df に対して

s2 = pd.Series(range(3), index=[0, 1, 3])
df["s"] = s2
df
#    a    s
# 0  0  0.0
# 1  1  1.0
# 2  2  NaN

ということも起こりえます。

つまり、Series を DataFrame に代入する場合、後者の index に合致する要素が前者から取り出されている、ということです。

Series の index には興味がなく、元データの順番を保ったまま DataFrame に入れたい、というようなときは、Series.reset_index(drop=True) してから代入すれば解決できます。

df["s2"] = s.reset_index(drop=True)
df
#    a    s  s2
# 0  0  0.0   0
# 1  1  1.0   1
# 2  2  NaN   2
余談:なんでこんな挙動が起こるか?

なお、ここまで書いてきた挙動は pandas.core.frame の内部で定義されたもので、具体的には __setitem___set_item_sanitize_column の順に行き着くサニタイズ関数が処理の主体です。この関数内部で

def reindexer(value):
    # reindex if necessary

    if value.index.equals(self.index) or not len(self.index):
        value = value._values.copy()
    else:

        # GH 4107
        try:
            value = value.reindex(self.index)._values
        except Exception as e:

            # duplicate axis
            if not value.index.is_unique:
                raise e

            # other
            raise TypeError('incompatible index of inserted column '
                            'with frame index')
    return value

なる別の関数が定義されており、try 節の初めで value(この記事でいう Series)の再インデクシングが行われていますね。reindex は引数に存在しないインデックスに対応する値を nan にしてしまうため、上記のような現象が起こります。

ちなみに、このあたりの変更は少なくとも2016年、さらにおそらく2014年頃まで遡れるので、相当基本的な挙動として想定されているものだと言えそうです。