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年頃まで遡れるので、相当基本的な挙動として想定されているものだと言えそうです。