27
22

More than 3 years have passed since last update.

僕のpandas.SeriesとDataFrameのイメージは間違っていた

Last updated at Posted at 2019-04-11

Pandas の入門記事には、大抵こんなことが書かれています。

  • Series は一次元配列です。組み込み型のlistのようなものです。
  • DataFrame は二次元配列です。

私も「ふーん、なるほど」と理解したつもりになって Pandas を使い始めたのですが、あとでとんでもない思い違いをしていたの気づきました。

Series はリストではないし、DataFrame は二次元配列ではないのです

Series・DataFrameの間違ったイメージ

私の間違ったイメージはこうでした:

SeriesやDataFrameは配列のようなものだ、だから s[i], df[i] で i+1 番目の値・行にアクセスできる

図で書くとこんな感じです:

間違ったpandas (1).png

まぁ、このイメージでも、使い始めてしばらくは何とかなりました。

しかしインデックスが現れると、私の間違ったイメージは破綻しました。

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

図で表すとこうです:

正しいpandas (1).png

SeriesDataFrameで明示的にインデックスを指定しないと、添字と同じ連番が使われるので、たまたま「Seriesはlistのようなもの」と思ってもうまく行きました。

しかし、本来インデックス≠添字なので、インデックスに連番数値以外のものも指定できるのです。

非数値インデックスによるアクセス (1).png

インデックスあれこれ

一見、非直感的な「インデックス≠添字」ですが、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 は二次元配列ではない!


  1. 内部的にはインデックス値→添字の変換をしていると思いますが、外からは添字を意識しなくてもよい作りになっています。 

27
22
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
27
22