Pandas の入門記事には、大抵こんなことが書かれています。
Series
は一次元配列です。組み込み型のlist
のようなものです。DataFrame
は二次元配列です。
私も「ふーん、なるほど」と理解したつもりになって Pandas を使い始めたのですが、あとでとんでもない思い違いをしていたの気づきました。
Series はリストではないし、DataFrame は二次元配列ではないのです。
Series・DataFrameの間違ったイメージ
私の間違ったイメージはこうでした:
SeriesやDataFrameは配列のようなものだ、だから
s[i]
,df[i]
で i+1 番目の値・行にアクセスできる。
図で書くとこんな感じです:
まぁ、このイメージでも、使い始めてしばらくは何とかなりました。
しかしインデックスが現れると、私の間違ったイメージは破綻しました。
s = Series(['val0', 'val1', 'val2', 'val3', 'val4'], index=['a', 'b', 'c', 'd', 'e'])
# `[]`の中が数値ではないのに、要素にアクセスできる!?
s['c'] # => 'val2'
Series・DataFrameの正しいイメージ
私の誤解は、「s[i]
, df[i]
は i+1 番目の値・行にアクセスしている」ということでした。
実際には s[i]
の i
は添字(位置)ではなくインデックスの値なのです。s[i]
にアクセスするのに、添字は関係ないのです1。
図で表すとこうです:
Series
やDataFrame
で明示的にインデックスを指定しないと、添字と同じ連番が使われるので、たまたま「Seriesはlistのようなもの」と思ってもうまく行きました。
しかし、本来インデックス≠添字なので、インデックスに連番数値以外のものも指定できるのです。
インデックスあれこれ
一見、非直感的な「インデックス≠添字」ですが、Pandasはインデックスの存在が前提の作りになっていて、それにより効率的なデータ操作ができるようになっています。
以下では、(昔の私のように)「インデックスは添字のこと」という理解だと、思っていたのと違う動作になってしまったり、無駄な書き方をしてしまうような例を紹介します。
DataFrame を作ると Series のインデックスが自動的に使われる
DataFrameを作る時、各列のSeriesのインデックスが同じものであれば、index=
を指定しなくても、DataFrameに同じインデックスが使われます。
from pandas import *
s1 = Series(['val1', 'val2', 'val3'], index=['a', 'b', 'c'])
s2 = Series(['val10', 'val20', 'val30'], index=['a', 'b', 'c'])
df = DataFrame({'Col1': s1, 'Col2': s2})
# df = DataFrame({'Col1': s1, 'Col2': s2}, index=['a', 'b', 'c']) # インデックスを明示しなくても同じ
print(df) # 添字(0, 1, 2) ではなく、'a', 'b', 'c' がインデックスになる
# Col1 Col2
# a val1 val10
# b val2 val20
# c val3 val30
異なるインデックスの Series から DataFrame を作ると、インデックスがマージされる
各列のSeriesが違うインデックスを持っているときは、マージされた新しいインデックスが作られます。このとき、インデックスに対応する値が無い部分は na になります。
from pandas import *
s1 = Series(['val1', 'val2', 'val3'], index=['a', 'b', 'c'])
s2 = Series(['val10', 'val20', 'val30'], index=['c', 'd', 'e'])
df = DataFrame({
'Col1': s1,
'Col2': s2,
})
print(df)
# Col1 Col2
# a val1 NaN
# b val2 NaN
# c val3 val10
# d NaN val20
# e NaN val30
インデックスに異なる型が混じっていてもエラーにはならない
Series
には文字列と数値のような異なる型を混ぜて格納できますが、インデックスも同様です。そのため、DataFrameを作るときに各列のインデックスの型が異なっていてもエラーにはなりません。
from pandas import *
s1 = Series(['val1', 'val2', 'val3'], index=['a', 'b', 'c'])
s2 = Series(['val10', 'val20', 'val30'], index=[1, 2, 3])
df = DataFrame({
'Col1': s1,
'Col2': s2,
})
print(df)
# Col1 Col2
# a val1 NaN
# b val2 NaN
# c val3 NaN
# 1 NaN val10
# 2 NaN val20
# 3 NaN val30
DataFrame の特定の列をインデックスにできる
インデックスは添字ではなく「特別扱いされる列」のようなものです。DataFrameを作った後に、インデックスを変更したり、特定の列をインデックスに指定したりできます。
from pandas import *
df = DataFrame({
'amount': [1000, 2000, 10000],
'name': ['北里', '津田', '渋沢'],
'birthyear': [1853, 1864, 1840],
})
print(df.loc[0]) # デフォルトでは連番がインデックスになる
# amount 1000
# name 北里
# birthyear 1853
# Name: 0, dtype: object
df = df.set_index('amount') # 特定の列をインデックスに変えられる
print(df.loc[1000])
# name 北里
# birthyear 1853
# Name: 1000, dtype: object
インデックスには重複値があってもよい
インデックスには同じ値が2度出現してもエラーにはなりません。
from pandas import *
# インデックスに重複値があってもよい
s = Series(['val1', 'val2', 'val3', 'val4', 'val5'], index=['a', 'b', 'b', 'c', 'b'])
print(s['a']) # 重複していないインデックス値ではそのまま取り出せる
# val1
print(type(s['b'])) # 重複したインデックス値では、戻り値がSeriesになる
# <class 'pandas.core.series.Series'>
print(s['b'])
# b val2
# b val3
# b val5
# dtype: object
ただし、DataFrameを作るためにインデックスのマージが起きるときには、重複値があるとエラーになります。
from pandas import *
# インデックスに重複値があってもよい
s1 = Series(['val1', 'val2', 'val3', 'val4', 'val5'], index=['a', 'b', 'b', 'c', 'b'])
s2 = Series(['valA', 'valB', 'valC', 'valD', 'valE'], index=['a', 'b', 'b', 'c', 'b'])
s3 = Series(['val10', 'val20', 'val30'], index=['a', 'b', 'c'])
# 同じインデックス同士なら、重複値があってもエラーにならない
df = DataFrame({
'Col1': s1,
'Col2': s2,
})
print(df)
# Col1 Col2
# a val1 valA
# b val2 valB
# b val3 valC
# c val4 valD
# b val5 valE
# 異なるインデックス同時だと、重複値があるとエラーになる
df = DataFrame({
'Col1': s1,
'Col3': s3,
})
# => ValueError: cannot reindex from a duplicate axis
インデックスを無視して添字でアクセスできる
時にはインデックスを無視して添字(位置)でアクセスしたくなることもありますが、Series・DataFrameともに、.iloc
で添字アクセス可能です。
from pandas import *
s = Series(['val1', 'val2', 'val3'], index=['a', 'b', 'c'])
print(s.iloc[1]) # 添字(位置)でアクセスできる
df = DataFrame({
'index': ['a', 'b', 'c'],
'Col1': Series(['val1', 'val2', 'val3']),
'Col2': Series(['valA', 'valB', 'valC']),
}).set_index('index')
print(df.iloc[2]) # 添字(位置)でアクセスできる
# Col1 val3
# Col2 valC
# Name: c, dtype: object
良心的な入門記事の見分け方
さて、冒頭で取り上げた、Series・DataFrameの間違った説明ですが、
Series
は一次元配列です。組み込み型のlistのようなものです。DataFrame
は二次元配列です。
入門者にとりあえずイメージを掴んでもらう方便としては、そんなに的外れでもありません。インデックスが添字である間は矛盾は生じません。ただ、世の中には書き手が pandas を理解していないのか、「方便」を訂正せずに進んでしまうものもあるようです。
上記の説明の後にこんな但し書きがあるかどうかで、良心的な記事を判別できるでしょう。
ただし、連番以外の「インデックス」を持つことができるなど、リストや二次元配列より高機能です。インデックスについては第2章で説明します。
まとめ
Series はリストではない!DataFrame は二次元配列ではない!
-
内部的にはインデックス値→添字の変換をしていると思いますが、外からは添字を意識しなくてもよい作りになっています。 ↩