SeriesとDataFrame — pandasの2つの基本構造
列とは何か、表とは何か、そしてなぜ列内のすべての値が1つの型を共有しなければならないのかを学びます。df["col"]とdf[["col"]]の違いも解説します。
どうやって学びますか?
解決する問題
前の章で、次のような1行のコードを書き、それが実際に動作しました:
print(sales["quantity"].sum())19この行は直感的に読めますが、そこにはある疑問が隠されています。sales["quantity"]は何を返したのでしょうか?リストでしょうか?しかしリストには.sum()がありません — Pythonで[12, 5, 2].sum()と書くとエラーになります。
この答えを知っておく価値は大いにあります。なぜなら、pandasで手元にあるのは常に2つのうちのどちらかであり、ほぼすべての混乱は「今、手元にあるのはどちらなのか?」という同じ疑問から始まるからです。1つの列でしょうか、それとも表全体でしょうか?
この章では、その2つの対象について学びます。それぞれの名前、形、そして両者を分ける違いについて見ていきましょう。
この章を終える頃には
SeriesとDataFrameを分ける違いを1文で説明できるようになるSeriesを作成し、そのインデックス、値、型を個別に確認できるようになる- 列内のすべての値が1つの型を共有しなければならない理由を説明できるようになる
df["col"]とdf[["col"]]の違いを理解した上で使い分けられるようになるKeyErrorを読み解き、列名に何が起きたのかを正確に特定できるようになる
前提条件: pandasのインストールと初めての表。
Series — 1つの列
まずはより小さな単位から始めましょう。Seriesとは、それぞれに名前が対応づけられた値の並びです:
import pandas as pd
quantity = pd.Series([12, 5, 2])
print(quantity)0 12
1 5
2 2
dtype: int643つの値を渡したのに対して、出力されたのは3つのペアでした。左端に並んでいる0、1、2はインデックスであり、各値の名前です。これは自分で指定したものではなく、pandasが0から数えて自動的に割り当てたものです。
そして一番下にあるdtype: int64は、シリーズ全体の型を表しています。値ごとに異なる型を持つのではなく、全体で1つの型です。これこそがpandasの処理が高速である理由であり、同時に最大の制約でもあります。
この3つの要素は個別に確認することができます:
import pandas as pd
quantity = pd.Series([12, 5, 2])
print(quantity.index)
print(quantity.values)
print(quantity.dtype)RangeIndex(start=0, stop=3, step=1)
[12 5 2]
int64RangeIndexは、インデックスが0から始まって1ずつ増える単なるカウントにすぎないことを意味します。valuesは名前を取り除いた値そのものです。そしてdtypeは列全体の型です。
インデックスは数値である必要はありません:
import pandas as pd
quantity = pd.Series([12, 5, 2], index=["pen", "notebook", "bag"])
print(quantity)
print(quantity["notebook"])pen 12
notebook 5
bag 2
dtype: int64
5これで各行に商品名が付き、その名前を使って値を取り出せるようになりました。これはPythonの辞書のように思えるかもしれませんし、実際によく似ています。ただし、1つ大きな違いがあります:
import pandas as pd
quantity = pd.Series([12, 5, 2], index=["pen", "notebook", "bag"])
print(quantity * 2)pen 24
notebook 10
bag 4
dtype: int64ループを使わずに、列全体の値が一括して2倍になりました。辞書に対して同じことをしようとすれば、自分でループを回して各値を1つずつ処理しなければなりません。Seriesに対して操作を実行すると、pandasはそのすべての値に一括してそれを適用します — そして、これ以降のすべての章がこの挙動を前提として成り立っています。
DataFrame — 複数の列、1つのインデックス
DataFrameとは、1つのインデックスを共有して並んだ複数のSeriesのことです。
import pandas as pd
sales = pd.DataFrame({
"product": ["pen", "notebook", "bag"],
"quantity": [12, 5, 2],
"price": [15.0, 60.0, 850.0],
})
print(sales)
print(sales.shape)
print(sales.columns)product quantity price
0 pen 12 15.0
1 notebook 5 60.0
2 bag 2 850.0
(3, 3)
Index(['product', 'quantity', 'price'], dtype='object')この「1つのインデックスを共有する」という言葉こそが本質です。インデックス0におけるproduct、quantity、priceは、すべて同じ1本のペンという対象を表しています。このインデックスこそが、3つの列を行として結びつけているものなのです。
1つの表に複数の型 — ただし1つの列には1つの型
import pandas as pd
sales = pd.DataFrame({
"product": ["pen", "notebook", "bag"],
"quantity": [12, 5, 2],
"price": [15.0, 60.0, 850.0],
})
print(sales.dtypes)product object
quantity int64
price float64
dtype: object3つの列があり、3つの型があります。int64は整数、float64は小数、そしてobjectは通常、文字列を意味します。
strが存在しないことに注目してください。pandasの型はNumPyに由来しており、NumPyではすべての値が同じメモリサイズを占めるようになっています。これは数値では可能ですが、長さがまちまちな文字列では不可能です。そのため、文字列を保持するために、pandasは代わりにPythonオブジェクトへのポインタを保持する仕組みを採用しており、その構成がobjectと呼ばれます。
ここで重要なルールがあります。列内のすべての値は1つの型を共有するということです。では、数値と文字列が1つの列に混ざると何が起きるでしょうか?
import pandas as pd
mixed = pd.Series([1, "two", 3.0])
print(mixed)
print(mixed.dtype)0 1
1 two
2 3.0
dtype: objectpandasはエラーを出しません — 利用可能な最も緩い型へとフォールバックし、列全体をobjectにしてしまいます。その結果は派手なエラーではなく、静かに訪れます。その列はもはや数値型ではないため、.mean()の計算は失敗し、.sum()は文字列の連結を始めてしまいます。CSVのたった1行に紛れ込んだN/Aがまさにこれを引き起こします。そしてそれこそが第6章のテーマです。
手元にあるのはどちらか — SeriesかDataFrameか?
import pandas as pd
sales = pd.DataFrame({
"product": ["pen", "notebook", "bag"],
"quantity": [12, 5, 2],
"price": [15.0, 60.0, 850.0],
})
column = sales["quantity"]
print(type(column))
print(type(sales))
print(column.mean())<class 'pandas.core.series.Series'>
<class 'pandas.core.frame.DataFrame'>
6.333333333333333これで、この章の冒頭で提起した疑問への答えが出ました。sales["quantity"]はリストではなく、Seriesだったのです — そしてSeriesには.sum()、.mean()、.max()などが備わっています。
また、誰もが最初につまずく組み合わせがここにあります:
import pandas as pd
sales = pd.DataFrame({
"product": ["pen", "notebook", "bag"],
"quantity": [12, 5, 2],
"price": [15.0, 60.0, 850.0],
})
print(sales["quantity"])
print("---")
print(sales[["quantity"]])0 12
1 5
2 2
Name: quantity, dtype: int64
---
quantity
0 12
1 5
2 2角括弧が1組増えただけで、返ってくる結果の種類がまったく別のものになります。最初のものはSeriesです — 1つの列であり、その名前と型が下部に出力されます。2番目のものはDataFrameです — たまたま1列だけを持つ表であるため、列の見出しが上部に表示されます。
内側の角括弧は列のリストを意味し、リストを要求すれば表が返ってくる、というのがここでのルールです。そのため、sales[["product", "price"]]とすれば2列の表が得られます。
なぜこの違いを意識しておく必要があるのか? それは、エラーメッセージが枝分かれする分岐点だからです。Seriesには.mean()がありますが列名はありません。一方、DataFrameは列を選択できますが、単一の数値を直接返してはくれません。あるオブジェクトにそのような属性はないというメッセージが表示されたとき、最初に問いかけるべきは「今、実際に手元にあるのは2つのうちのどちらなのか?」ということです。
完全な例
inventory.py:
import pandas as pd
# Product names become the index, so a row can be reached by name.
stock = pd.DataFrame(
{
"quantity": [12, 5, 2, 30],
"price": [15.0, 60.0, 850.0, 8.0],
},
index=["pen", "notebook", "bag", "eraser"],
)
print(stock)
print()
print("Shape :", stock.shape)
print("Columns :", list(stock.columns))
print("Index :", list(stock.index))
print()
# A column is a Series, so an operation applies to every value at once.
value = stock["quantity"] * stock["price"]
print("Value per product:")
print(value)
print()
print("Total value:", value.sum())quantity price
pen 12 15.0
notebook 5 60.0
bag 2 850.0
eraser 30 8.0
Shape : (4, 2)
Columns : ['quantity', 'price']
Index : ['pen', 'notebook', 'bag', 'eraser']
Value per product:
pen 180.0
notebook 300.0
bag 1700.0
eraser 240.0
dtype: float64
Total value: 2420.0注目すべき点が2つあります。
第1に、stock["quantity"] * stock["price"]は2つのSeriesを掛け合わせ、新たなSeriesを生成しました。pandasはそれらをインデックスによって対応づけました — ペンの数量にはペンの価格を、バッグの数量にはバッグの価格を掛け合わせたのです。行の並び順ではなく、名前によって一致させています。この挙動は、後で2つの表を結合する際の基礎となります。
第2に、quantityはint64でしたが、結果として得られた列の型はfloat64になっています。整数に小数を掛けると小数になる — これは通常のPythonとまったく同じルールであり、pandasが独自に特別な処理をしているわけではありません。
問題が起きたとき
KeyError: 'Quantity' そのような列は存在しません。列名は大文字と小文字が区別され、CSVファイルから読み込んだ列名には前後に空白が含まれていることもよくあります。print(list(df.columns))を実行して、実際にどのような名前になっているかを正確に確認してください — リストとして出力すると各名前がクォートで囲まれるため、余分な空白が目に見えるようになります。
AttributeError: 'DataFrame' object has no attribute 'mean_price' df.column_nameという書き方でも列を取得できますが、これは単純な名前にしか使えません — 名前に空白が含まれている場合や、数字で始まっている場合、あるいはcountなどのpandasのメソッド名と衝突している場合には使えません。df["column_name"]なら常に正しく機能するため、こちらの記法を使う習慣をつけましょう。
数値列の計算中に発生するTypeError: unsupported operand type(s) その列はおそらく数値型ではありません。df.dtypesを確認してください。そこにobjectが表示されているなら、どこかに文字列が混ざっています。
.sum()が数値を足し合わせずに文字列を連結してしまう 原因は同じです — 列がobjectになっています。たった1行にN/Aや空のセルが1つあるだけで、列全体の型が変わってしまいます。
ValueError: All arrays must be of the same length pd.DataFrame({...})に渡されたリストの長さが揃っていません。行が1つの共通のインデックス上に揃う必要があるため、すべての列で値の数が同じでなければなりません。
ステップ 4 / 6 — 予想する
理解度をチェック
3つの値が渡されました。正確には何が出力されますか?
import pandas as pd
quantity = pd.Series([12, 5, 2])
print(quantity)- A12 5 2
- B[12, 5, 2]
- C12 5 2 dtype: int64
- D0 12 1 5 2 2 dtype: int64
2つの行の違いは角括弧が1組余分にあることだけです。type()は何を報告しますか?
import pandas as pd
sales = pd.DataFrame({
"product": ["pen", "notebook", "bag"],
"quantity": [12, 5, 2],
"price": [15.0, 60.0, 850.0],
})
print(type(sales["quantity"]))
print(type(sales[["quantity"]]))- A両方ともSeries
- B両方ともDataFrame
- C1つ目はSeries、2つ目はDataFrame
- D1つ目はDataFrame、2つ目はSeries
1つの列に数値と文字列が混ざっています。pandasはどう処理しますか?
import pandas as pd
mixed = pd.Series([1, "two", 3.0])
print(mixed.dtype)- Aエラーは出さず、列全体の型を`object`にする
- B`TypeError` — 1つの列に2つの型を共存させることはできない
- C文字列を除外し、列を`float64`として保持する
- D値ごとに個別の型を保持する
回答にはアカウントが必要です
ログインして答えを確認
問題は上にあります。頭の中で答えを考えることがいちばん大切です。ログインすると、正解・解説・3 段階のヒントを確認できます。
やってみよう
5冊以上の本を含むDataFrameを保持するlibrary.pyというファイルを作成してください — 書名をインデックスとし、冊数(整数)、価格(小数)、著者名(文字列)の列を用意します。
作成したら、以下を出力してください:
- 表全体、その
shape、およびdtypes - 価格列単体 — そして
type()を使ってそれがSeriesであることを示す - 二重の角括弧を使った価格列 — そしてこちらは
DataFrameであることを示す - 各本の合計金額(冊数 × 価格)、および全書籍の総額
その後、1つの実験を行ってみてください。冊数のうちの1つを"unknown"に書き換えて、もう一度ファイルを実行します。dtypesの表示はどう変わったでしょうか?そして合計値はどうなりましたか?
この実験こそが、この章で最も価値のある5分間です。1つのセルのたった1つの単語が列全体の型を変えてしまい、何のエラーも出さずに計算結果を狂わせてしまう — そして現実のデータでは、それが日常茶飯事のように起きるのです。