Pandas の基本的なデータ構造、インデックス操作、集計、欠損値処理、結合、グループ化について、順を追って学べるように整理しています。
参考として、次の記事も参照してください。
NumPyライブラリ紹介
Matplotlibライブラリ紹介
データサイエンティストのためのPythonライブラリ応用(上級編)
導読
- この文は、Pandas を使う理由 -> オブジェクトを作る -> DataFrame を読む -> 計算と集計を行う -> 欠損値を処理する -> データを結合する -> グループ化とピボットテーブルへ進む、という流れで Pandas の全体像を最初に押さえるためのメモです。
- 読み始めるときは、まず
Series、DataFrame、index、columns、loc、iloc、isnull()、groupby()、merge()、pivot_table()を優先して押さえると、その後の分析コードをかなり読みやすくできます。 - NumPy が数値計算の土台を担うのに対して、Pandas はラベル付きデータを扱うための実務的な操作をまとめて担います。列名や行名を持ったまま集計や結合ができるのが大きな強みです。
この文の章立ては、次のようになっています。
- Pandas を使う理由
- NumPy と Pandas の役割の違い: NumPy が得意なことと、Pandas が補う部分を整理します。
- Pandas オブジェクトの作成
- Series を作る:
pd.Series()を使って、リスト、NumPy 配列、辞書、スカラーから作成します。 - DataFrame を作る:
pd.DataFrame()を使って、Series、辞書、辞書のリスト、二次元 NumPy 配列から作成します。
- Series を作る:
- DataFrame の性質と基本操作
- 属性を確認する:
values、index、columns、shape、size、dtypesを確認します。 - インデックスで取り出す: 列、行、スカラーを
[]、loc、ilocで取り出します。 - スライスと選択: 行切り出し、列切り出し、行列同時指定、散在した選択を行います。
- ブールインデックスと代入: 条件抽出、
isin()、新しい列の追加、値の更新、indexとcolumnsの変更を扱います。
- 属性を確認する:
- 数値演算と統計分析
- データを確認する:
head()、tail()、info()で全体をすばやく確認します。 - NumPy 関数をそのまま使う: ベクトル化演算、行列演算、ブロードキャストを Pandas 上で行います。
- インデックスの自動整列: ラベルに基づいて演算がそろう仕組みを確認します。
- 並べ替えと統計量:
value_counts()、sort_values()、sort_index()、count()、sum()、mean()、describe()、corr()などを扱います。 -
apply()で自作集計を行う: 列方向(横)や行方向(縦)に独自の関数を適用します。
- データを確認する:
- 欠損値処理
- 欠損値を見つける:
isnull()、notnull()、dtypesを見ながら欠損を確認します。 - 欠損値を削除する:
dropna()で行や列を落とします。 - 欠損値を埋める:
fillna()で定数や平均値を使って補完します。
- 欠損値を見つける:
- データの結合
-
concat()で縦横に結合する: 行方向(縦)、列方向(横)、重複インデックス時の扱いを見ます。 -
merge()でキーをそろえて結合する: 共通キーをもつ表を結び付けます。
-
- グループ化とピボットテーブル
-
groupby()の基本: 集約、反復、aggregate()、filter()、transform()、apply()を確認します。 - グループキーを工夫する: リスト、辞書、関数、複数キーでのグループ化を扱います。
- 実例で理解する: 惑星データとタイタニックデータで
groupby()とpivot_table()を使います。
-
- そのほかの機能
- 文字列、時系列、多重インデックス: Pandas の応用範囲を見渡します。
-
eval()とquery(): 式評価や条件抽出を高速化する方法と使いどころを確認します。
Pandas を使う理由
NumPy は、ベクトル化された数値計算を非常に高速に処理できます。一方で、実務のデータ分析では、列名を付ける、欠損値を扱う、カテゴリごとに集計する、ピボットテーブルを作るといった操作が頻繁に必要になります。
Pandas は NumPy を土台にしつつ、こうしたラベル付きデータの操作を扱いやすくしたライブラリです。数値だけではなく、文字列や日時を含む表形式データを一貫した書き方で扱えるのが強みです。
import numpy as np
import pandas as pd
# NumPy は数値配列の計算に強い
x = np.arange(6).reshape(2, 3)
print(x)
print(x.sum(axis=0))
# Pandas は列名を持った表として扱える
df = pd.DataFrame(x, columns=["A", "B", "C"])
print(df)
print(df.sum())
Pandas オブジェクトの作成
この章では、Pandas の基本となる Series と DataFrame をどのように作るかを整理します。どんな入力から作れるかを把握しておくと、CSV 読み込み後の変換や手元での検証コードが書きやすくなります。
Series を作る
Series は、ラベル付きの一次元配列です。値の並びだけではなく、各値に index が付いているのが特徴です。
共通の形は次のとおりです。
pd.Series(data, index=index, dtype=dtype)
data にはリスト、辞書、NumPy 配列、スカラーを渡せます。index と dtype はどちらも省略可能です。
リストから作る
まずはリストから作るのが基本です。index を省略すると 0 から始まる整数インデックスが付きます。
import pandas as pd
# index を省略すると 0 から始まる整数が付く
data = pd.Series([1.5, 3, 4.5, 6])
print(data)
# index を自分で付けることもできる
data = pd.Series([1.5, 3, 4.5, 6], index=["a", "b", "c", "d"])
print(data)
# dtype を指定して型をそろえる
data = pd.Series([1, 2, 3, 4], index=["a", "b", "c", "d"], dtype="float")
print(data)
Series は異なる型の値も持てますが、その場合は object 型として扱われることがあります。数値として扱いたいときは dtype をそろえる意識が重要です。
import pandas as pd
# 文字列が混ざると object 型になることがある
data = pd.Series([1, 2, "3", 4], index=["a", "b", "c", "d"])
print(data)
print(data["a"])
print(data["c"])
# 変換可能な文字列なら dtype=float を指定して数値化できる
data = pd.Series([1, 2, "3", 4], index=["a", "b", "c", "d"], dtype=float)
print(data)
print(data["c"])
# 変換できない値を含むとエラーになる
# pd.Series([1, 2, "a", 4], index=["a", "b", "c", "d"], dtype=float)
NumPy 配列から作る
一次元の NumPy 配列からも、そのまま Series を作れます。
import numpy as np
import pandas as pd
# NumPy の一次元配列からも Series を作れる
x = np.arange(5)
print(pd.Series(x))
辞書から作る
辞書を渡すと、キーが index、値が data として使われます。index を指定すると、辞書に存在しないキーは NaN で埋められます。
import pandas as pd
population_dict = {
"東京": 3341,
"大阪": 1296,
"名古屋": 714,
"福岡": 222,
}
# 辞書のキーが index、値がデータになる
population = pd.Series(population_dict)
print(population)
# 存在しないラベルは NaN で埋まる
population = pd.Series(population_dict, index=["東京", "名古屋", "神戸", "仙台"])
print(population)
スカラーから作る
値がスカラーの場合は、指定した index の長さに合わせて同じ値が並びます。
import pandas as pd
# スカラーを渡すと index の長さだけ同じ値が並ぶ
print(pd.Series(5, index=[100, 200, 300]))
DataFrame を作る
DataFrame は、行と列のラベルを持った二次元の表形式データです。Pandas で最もよく使う構造で、分析の中心になります。
共通の形は次のとおりです。
pd.DataFrame(data, index=index, columns=columns)
Series から作る
Series ひとつからも DataFrame を作れます。列名を後から指定することもできます。
import pandas as pd
population_dict = {
"東京": 3341,
"大阪": 1296,
"名古屋": 714,
"福岡": 222,
}
population = pd.Series(population_dict)
# Series 1 本からでも DataFrame を作れる
print(pd.DataFrame(population))
# columns で列名を明示する
print(pd.DataFrame(population, columns=["population"]))
Series の辞書から作る
複数の Series を辞書にして渡すと、列ごとの意味を持った DataFrame ができます。長さが合わない部分は自動的に NaN で補われます。スカラーを渡すと、その値が全行へ広がります。
import pandas as pd
population_dict = {
"東京": 3341,
"大阪": 1296,
"名古屋": 714,
"福岡": 222,
}
GDP_dict = {
"東京": 270,
"大阪": 95,
"名古屋": 70,
"福岡": 30,
}
population = pd.Series(population_dict)
GDP = pd.Series(GDP_dict)
# 列ごとの意味を持った表になる
print(pd.DataFrame({"population": population, "GDP": GDP}))
# スカラーは全行へ自動的に広がる
print(pd.DataFrame({"population": population, "GDP": GDP, "country": "日本"}))
辞書のリストから作る
行単位の辞書を並べると、そのまま表として読み込めます。キーがそろっていない部分は NaN になります。
import pandas as pd
# 行単位の辞書を並べると表として作成できる
data = [{"a": i, "b": 2 * i} for i in range(3)]
data = pd.DataFrame(data)
print(data)
# 列を取り出してコピーしてから変更する
data1 = data["a"].copy()
print(data1)
data1[0] = 10
print(data1)
print(data)
# キーがそろわない部分は NaN になる
data = [{"a": 1, "b": 1}, {"b": 3, "c": 4}]
print(pd.DataFrame(data))
二次元 NumPy 配列から作る
NumPy の二次元配列を、そのまま DataFrame に載せることもできます。このとき columns や index を指定すると表らしく扱いやすくなります。
import numpy as np
import pandas as pd
data = np.random.randint(10, size=(3, 2))
print(data)
# columns と index を付けると表として読みやすくなる
print(pd.DataFrame(data, columns=["foo", "bar"], index=["a", "b", "c"]))
DataFrame の性質と基本操作
この章では、作成した DataFrame をどう読むか、どう取り出すか、どう更新するかをまとめます。ここを理解すると、Pandas を「ラベル付きの表」として自然に扱えるようになります。
属性を確認する
DataFrame を受け取ったら、まず values、index、columns、shape、size、dtypes を確認すると全体像をつかみやすくなります。
import pandas as pd
population_dict = {
"東京": 3341,
"大阪": 1296,
"名古屋": 714,
"福岡": 222,
}
GDP_dict = {
"東京": 270,
"大阪": 95,
"名古屋": 70,
"福岡": 30,
}
population = pd.Series(population_dict)
GDP = pd.Series(GDP_dict)
data = pd.DataFrame({"pop": population, "GDP": GDP})
# DataFrame の基本属性を順に確認する
print(data)
print(data.values)
print(data.index)
print(data.columns)
print(data.shape)
print(data.size)
print(data.dtypes)
列、行、スカラーを取り出す
Pandas のインデックス操作では、列、行、スカラーの取り出し方を分けて覚えると整理しやすくなります。
列は [] でも属性形式でも取り出せますが、確実なのは data["列名"] です。
import pandas as pd
population_dict = {
"東京": 3341,
"大阪": 1296,
"名古屋": 714,
"福岡": 222,
}
GDP_dict = {
"東京": 270,
"大阪": 95,
"名古屋": 70,
"福岡": 30,
}
population = pd.Series(population_dict)
GDP = pd.Series(GDP_dict)
data = pd.DataFrame({"pop": population, "GDP": GDP})
# 列を 1 本取り出す
print(data["pop"])
# 複数列もまとめて指定できる
print(data[["GDP", "pop"]])
# 属性形式でも取れるが、列名と衝突しない前提になる
print(data.GDP)
行はラベルベースの loc と、位置ベースの iloc を使い分けます。スカラーを取り出すときも loc[row, col] と iloc[row, col] が基本です。
import pandas as pd
population_dict = {
"東京": 3341,
"大阪": 1296,
"名古屋": 714,
"福岡": 222,
}
GDP_dict = {
"東京": 270,
"大阪": 95,
"名古屋": 70,
"福岡": 30,
}
population = pd.Series(population_dict)
GDP = pd.Series(GDP_dict)
data = pd.DataFrame({"pop": population, "GDP": GDP})
# loc はラベル指定
print(data.loc["東京"])
print(data.loc[["東京", "名古屋"]])
# iloc は位置指定
print(data.iloc[0])
print(data.iloc[[1, 3]])
# スカラーも行と列を組み合わせて取得する
print(data.loc["東京", "GDP"])
print(data.iloc[0, 1])
print(data.values[0][1])
print(type(data.GDP))
print(GDP)
print(GDP["東京"])
スライスと複合的な選択
日付インデックスを持つ DataFrame では、行スライスや列スライスを直感的に書けます。loc はラベルベース、iloc は位置ベースです。
import numpy as np
import pandas as pd
dates = pd.date_range(start="2019-01-01", periods=6)
df = pd.DataFrame(np.random.randn(6, 4), index=dates, columns=["A", "B", "C", "D"])
print(df)
# 日付ラベルで行を切り出す
print(df["2019-01-01":"2019-01-03"])
print(df.loc["2019-01-01":"2019-01-03"])
print(df.iloc[0:3])
# 列方向(横)のスライス
print(df.loc[:, "A":"C"])
print(df.iloc[:, 0:3])
# 行と列を同時に指定する
print(df.loc["2019-01-02":"2019-01-03", "C":"D"])
print(df.iloc[1:3, 2:])
# 離れた行や列も選べる
print(df.loc["2019-01-04":"2019-01-06", ["A", "C"]])
print(df.iloc[3:, [0, 2]])
print(df.loc[["2019-01-02", "2019-01-06"], "C":"D"])
print(df.iloc[[1, 5], 0:3])
print(df.loc[["2019-01-04", "2019-01-06"], ["A", "D"]])
print(df.iloc[[1, 5], [0, 3]])
ブールインデックス
条件式をそのまま DataFrame や Series にかけると、ブール配列を使って行やセルを抽出できます。isin() も実務でよく使う書き方です。
import numpy as np
import pandas as pd
dates = pd.date_range(start="2019-01-01", periods=6)
df = pd.DataFrame(np.random.randn(6, 4), index=dates, columns=["A", "B", "C", "D"])
print(df)
# 条件式そのものが True / False の表になる
print(df > 0)
# True の場所だけ値を残す
print(df[df > 0])
print(df.A > 0)
# 列 A が正の行だけ残す
print(df[df.A > 0])
df2 = df.copy()
df2["E"] = ["one", "one", "two", "three", "four", "three"]
print(df2)
# isin() で複数候補に一致する行を調べる
ind = df2["E"].isin(["two", "four"])
print(ind)
print(df2[ind])
代入とラベルの更新
Pandas では、新しい列の追加、セルの更新、index や columns の再設定も簡単に行えます。
import numpy as np
import pandas as pd
dates = pd.date_range(start="2019-01-01", periods=6)
df = pd.DataFrame(np.random.randn(6, 4), index=dates, columns=["A", "B", "C", "D"])
print(df)
s1 = pd.Series([1, 2, 3, 4, 5, 6], index=pd.date_range("20190101", periods=6))
# 新しい列を追加する
df["E"] = s1
print(df)
# 特定セルや列全体を更新する
df.loc["2019-01-01", "A"] = 0
df.iloc[0, 1] = 0
df["D"] = np.array([5] * len(df))
print(df)
# index や columns 自体も付け替えられる
df.index = [i for i in range(len(df))]
print(df)
df.columns = [i for i in range(df.shape[1])]
print(df)
数値演算と統計分析
Pandas は表形式データの操作が得意ですが、NumPy の計算能力とも非常に相性がよいです。この章では、確認系メソッド、数値演算、統計量、apply() をまとめて見ます。
データをすばやく確認する
head()、tail()、info() は、データを読み込んだ直後に最初に使う定番の確認方法です。
import numpy as np
import pandas as pd
dates = pd.date_range(start="2019-01-01", periods=6)
df = pd.DataFrame(np.random.randn(6, 4), index=dates, columns=["A", "B", "C", "D"])
print(df)
# 先頭と末尾をざっと見る
print(df.head())
print(df.head(2))
print(df.tail())
print(df.tail(3))
df.iloc[0, 3] = np.nan
print(df)
# info() で件数や型、欠損の有無を確認する
print(df.info())
NumPy の関数は Pandas にもそのまま使える
NumPy のユニバーサル関数は、Pandas の Series や DataFrame に対してもそのまま使えることが多いです。四則演算や指数関数などはそのまま書けます。
import numpy as np
import pandas as pd
x = pd.DataFrame(np.arange(4).reshape(1, 4))
print(x)
# スカラー演算や ufunc はそのまま使える
print(x + 5)
print(np.exp(x))
y = pd.DataFrame(np.arange(4, 8).reshape(1, 4))
print(y)
# 同じ形なら要素ごとの積になる
print(x * y)
行列演算と NumPy との比較
DataFrame に対しても転置や行列積を行えます。ただし、純粋な計算速度だけを見ると NumPy の方が速いことが多く、Pandas はラベル付きデータ処理に強みがあります。
import numpy as np
import pandas as pd
np.random.seed(42)
x = pd.DataFrame(np.random.randint(10, size=(30, 30)))
z = x.T
print(z)
np.random.seed(1)
y = pd.DataFrame(np.random.randint(10, size=(30, 30)))
# dot() で行列積を計算する
print(x.dot(y))
# Pandas と NumPy の速度を比べる
%timeit x.dot(y)
%timeit np.dot(x, y)
x1 = np.array(x)
y1 = np.array(y)
%timeit x1.dot(y1)
%timeit np.dot(x1, y1)
%timeit np.dot(x.values, y.values)
x2 = list(x1)
y2 = list(y1)
x3 = []
y3 = []
for i in x2:
row = []
for j in i:
row.append(int(j))
x3.append(row)
for i in y2:
row = []
for j in i:
row.append(int(j))
y3.append(row)
def f(x, y):
res = []
# 三重ループで行列積を自前実装する
for i in range(len(x)):
row = []
for j in range(len(y[0])):
sum_row = 0
for k in range(len(x[0])):
sum_row += x[i][k] * y[k][j]
row.append(sum_row)
res.append(row)
return res
%timeit f(x3, y3)
計算だけに注目すると NumPy の方が有利ですが、列名やインデックスを保ったまま処理したいときは Pandas が便利です。
ブロードキャスト
Pandas でもブロードキャストに近い計算ができ、行方向(縦)・列方向(横)のどちらにそろえるかを axis で明示できます。
import numpy as np
import pandas as pd
np.random.seed(42)
x = pd.DataFrame(np.random.randint(10, size=(3, 3)), columns=list("ABC"))
print(x)
# 1 行目を基準に各列を割る
print(x.iloc[0])
print(x / x.iloc[0])
# axis=0 は行方向(縦)へそろえて計算する
print(x.A)
print(x.div(x.A, axis=0))
# axis=1 は列方向(横)へそろえて計算する
print(x.div(x.iloc[0], axis=1))
インデックスの自動整列
Pandas の演算では、値の位置ではなく index と columns のラベルが自動的にそろえられます。存在しない組み合わせは NaN になります。
import numpy as np
import pandas as pd
A = pd.DataFrame(np.random.randint(0, 20, size=(2, 2)), columns=list("AB"))
B = pd.DataFrame(np.random.randint(0, 10, size=(3, 3)), columns=list("ABC"))
print(A)
print(B)
# ラベルが一致する場所だけで計算される
print(A + B)
# fill_value を使うと欠けた場所を補って計算できる
print(A.add(B, fill_value=0))
print(A * B)
並べ替えと統計量
Pandas は、カテゴリ数の集計、行列の並べ替え、記述統計量の取得を一連で書けます。ここは実務でかなり使用頻度が高い部分です。
import numpy as np
import pandas as pd
from collections import Counter
y = np.random.randint(3, size=20)
print(y)
print(np.unique(y))
print(Counter(y))
y1 = pd.DataFrame(y, columns=["A"])
print(y1)
# value_counts() でカテゴリ数を数える
print(np.unique(y1))
print(y1["A"].value_counts())
import numpy as np
import pandas as pd
population_dict = {
"東京": 3341,
"大阪": 1296,
"名古屋": 714,
"福岡": 222,
}
GDP_dict = {
"東京": 270,
"大阪": 95,
"名古屋": 70,
"福岡": 30,
}
population = pd.Series(population_dict)
GDP = pd.Series(GDP_dict)
city_info = pd.DataFrame({"population": population, "GDP": GDP})
city_info["per_GDP"] = city_info["GDP"] / city_info["population"]
print(city_info)
# by は並べ替えの基準列、ascending=False は降順を表す
print(city_info.sort_values(by="per_GDP"))
print(city_info.sort_values(by="per_GDP", ascending=False))
import numpy as np
import pandas as pd
data = pd.DataFrame(np.random.randint(20, size=(3, 4)), index=[2, 1, 0], columns=list("CBAD"))
print(data)
# axis=1 は列方向(横)、ascending=False は逆順で並べ替える指定
print(data.sort_index())
print(data.sort_index(axis=1))
print(data.sort_index(axis=1, ascending=False))
import numpy as np
import pandas as pd
df = pd.DataFrame(np.random.normal(2, 4, size=(6, 4)), columns=list("ABCD"))
print(df)
# 列ごとの代表的な統計量を確認する
print(df.count())
print(df.sum())
print(df.sum(axis=1))
print(df.min())
print(df.max(axis=1))
print(df.idxmax())
print(df.mean())
print(df.var())
print(df.std())
print(df.median())
data = pd.DataFrame(np.random.randint(5, size=(10, 2)), columns=list("AB"))
print(data)
print(data.mode())
print(df.quantile(0.75))
print(df.describe())
data_2 = pd.DataFrame(
[["a", "a", "c", "d"], ["c", "a", "c", "b"], ["a", "a", "d", "c"]],
columns=list("ABCD"),
)
print(data_2)
print(data_2.describe())
print(df.corr())
print(df.corrwith(df["A"]))
apply() で独自の処理をまとめる
apply() を使うと、列ごと、または行ごとに同じ関数を適用できます。NumPy の関数も自作関数も渡せます。
import numpy as np
import pandas as pd
df = pd.DataFrame(np.random.normal(2, 4, size=(6, 4)), columns=list("ABCD"))
print(df)
# NumPy 関数も apply() に渡せる
print(df.apply(np.cumsum))
# axis=1 を付けると列ごとではなく行ごとに処理する
print(df.apply(np.cumsum, axis=1))
print(df.apply(sum))
print(df.sum())
print(df.apply(lambda x: x.max() - x.min()))
def my_describe(x):
# 自分で欲しい統計量だけを返す
return pd.Series(
[x.count(), x.mean(), x.max(), x.idxmin(), x.std()],
index=["count", "mean", "max", "idxmin", "std"],
)
print(df.apply(my_describe))
欠損値処理
実務データでは、欠損値の扱いがほぼ必ず必要になります。Pandas は欠損の発見、削除、補完までをまとめて行えます。
欠損値を見つける
None や np.nan が混ざると、列の型が object に寄ることがあります。まずは dtypes と isnull() を見て、どこに欠損があるかを確認します。
import numpy as np
import pandas as pd
data = pd.DataFrame(
np.array([[1, np.nan, 2], [np.nan, 3, 4], [5, 6, None]]),
columns=["A", "B", "C"],
)
print(data)
# 型と欠損位置をまとめて確認する
print(data.dtypes)
print(data.isnull())
print(data.notnull())
欠損値を削除する
dropna() は、欠損を含む行や列を落としたいときに使います。axis と how を組み合わせると、細かく条件を指定できます。
import numpy as np
import pandas as pd
data = pd.DataFrame(
np.array([
[1, np.nan, 2, 3],
[np.nan, 4, 5, 6],
[7, 8, np.nan, 9],
[10, 11, 12, 13],
]),
columns=["A", "B", "C", "D"],
)
print(data)
print(data.dtypes)
# 既定では axis=0 なので、欠損を含む行が落ちる
print(data.dropna())
# axis="columns" を指定すると列方向(横)を判定対象にする
print(data.dropna(axis="columns"))
data["D"] = np.nan
print(data)
# how="all" は全要素が欠損の列だけ削除、how="any" は 1 つでも欠損があれば削除
print(data.dropna(axis="columns", how="all"))
print(data.dropna(axis="columns", how="any"))
data.loc[3] = np.nan
print(data)
# how="all" なので、全部欠損の行だけが対象になる
print(data.dropna(how="all"))
欠損値を埋める
欠損を落としたくないときは fillna() を使います。固定値、列ごとの平均、全体平均など、状況に応じて埋め方を変えられます。
import numpy as np
import pandas as pd
data = pd.DataFrame(
np.array([
[1, np.nan, 2, 3],
[np.nan, 4, 5, 6],
[7, 8, np.nan, 9],
[10, 11, 12, 13],
]),
columns=["A", "B", "C", "D"],
)
print(data)
# value=5 のようにスカラーを渡すと、すべて同じ値で埋める
print(data.fillna(value=5))
# value に Series を渡すと、同名列ごとに別の値で埋められる
fill = data.mean()
print(fill)
print(data.fillna(value=fill))
# stack() で 1 本の Series にして全体平均を計算する
fill = data.stack().mean()
print(fill)
print(data.fillna(value=fill))
データの結合
複数の DataFrame をまとめるときは、concat() と merge() が基本になります。concat() は縦横に積み重ねる操作、merge() は共通キーで表を結び付ける操作です。
concat() で縦横に結合する
まずは練習用に DataFrame を返す小さな関数を用意しておくと、挙動を確認しやすくなります。
import pandas as pd
def make_df(cols, ind):
# 列名とインデックスから簡単な表を作る補助関数
data = {c: [str(c) + str(i) for i in ind] for c in cols}
return pd.DataFrame(data, ind)
print(make_df("ABC", range(3)))
縦方向の結合は axis=0、横方向の結合は axis=1 です。
import pandas as pd
def make_df(cols, ind):
data = {c: [str(c) + str(i) for i in ind] for c in cols}
return pd.DataFrame(data, ind)
df_1 = make_df("AB", [1, 2])
df_2 = make_df("AB", [3, 4])
print(df_1)
print(df_2)
# axis=0 が既定で、行方向(縦)へ積み重ねる
print(pd.concat([df_1, df_2]))
df_3 = make_df("AB", [0, 1])
df_4 = make_df("CD", [0, 1])
print(df_3)
print(df_4)
# axis=1 を指定すると列方向(横)へ連結する
print(pd.concat([df_3, df_4], axis=1))
インデックスや列名が重なる場合は、ignore_index=True などで整理できます。
import pandas as pd
def make_df(cols, ind):
data = {c: [str(c) + str(i) for i in ind] for c in cols}
return pd.DataFrame(data, ind)
df_5 = make_df("AB", [1, 2])
df_6 = make_df("AB", [1, 2])
# ignore_index=True で既存 index を捨てて 0, 1, 2... を振り直す
print(pd.concat([df_5, df_6]))
print(pd.concat([df_5, df_6], ignore_index=True))
df_7 = make_df("ABC", [1, 2])
df_8 = make_df("BCD", [1, 2])
print(pd.concat([df_7, df_8], axis=1))
print(pd.concat([df_7, df_8], axis=1, ignore_index=True))
merge() でキーをそろえて結合する
merge() は SQL の JOIN に近い考え方で、共通する列をキーにして表を結合します。
import pandas as pd
def make_df(cols, ind):
data = {c: [str(c) + str(i) for i in ind] for c in cols}
return pd.DataFrame(data, ind)
df_9 = make_df("AB", [1, 2])
df_10 = make_df("BC", [1, 2])
print(df_9)
print(df_10)
# 共通列 B をキーにする内部結合で、両方にある行だけ残る
print(pd.merge(df_9, df_10))
df_9 = make_df("AB", [1, 2])
df_10 = make_df("CB", [2, 1])
print(df_9)
print(df_10)
print(pd.merge(df_9, df_10))
都市データのような実例で見ると、どの行が残るかを理解しやすくなります。how="outer" を使うと、どちらか一方にしか存在しないキーも残せます。
import pandas as pd
population_dict = {
"city": ("東京", "名古屋", "福岡"),
"pop": (270, 70, 30),
}
population = pd.DataFrame(population_dict)
GDP_dict = {
"city": ("東京", "大阪", "名古屋"),
"GDP": (30320, 32680, 13468),
}
GDP = pd.DataFrame(GDP_dict)
print(population)
print(GDP)
# how を省略すると内部結合で、両方にある都市だけ残る
print(pd.merge(population, GDP))
# how="outer" にすると片方だけにある都市も残る
print(pd.merge(population, GDP, how="outer"))
グループ化とピボットテーブル
groupby() は Pandas の中でも特に重要な機能です。カテゴリごとに集計したり、グループ単位で変換したりできます。ピボットテーブルは、その集計結果を見やすい二次元表へ整えるための道具です。
groupby() の基本
groupby() は呼び出しただけでは結果を計算せず、集約メソッドを付けて初めて結果を返します。これが遅延評価のような使い方です。
import numpy as np
import pandas as pd
df = pd.DataFrame({
"key": ["A", "B", "C", "C", "B", "A"],
"data1": range(6),
"data2": np.random.randint(0, 10, size=6),
})
print(df)
# groupby オブジェクトを作るだけではまだ集計されない
print(df.groupby("key"))
print(df.groupby("key").sum())
print(df.groupby("key").mean())
# グループごとに中身を見てみる
for group in df.groupby("key"):
print(str(group))
# 特定列だけを抜き出して集計する
print(df.groupby("key")["data2"].sum())
for data, group in df.groupby("key"):
print("{0:5} shape={1}".format(data, group.shape))
print(df.groupby("key")["data1"].describe())
print(df.groupby("key").aggregate(["min", "median", "max"]))
filter()、transform()、apply()
filter() は条件に合うグループだけを残し、transform() は元の行数を保ったままグループ単位の変換を返します。apply() はさらに自由度が高く、任意の処理をまとめて書けます。
import numpy as np
import pandas as pd
df = pd.DataFrame({
"key": ["A", "B", "C", "C", "B", "A"],
"data1": range(6),
"data2": np.random.randint(0, 10, size=6),
})
def filter_func(x):
# 標準偏差が 3 を超えるグループだけ残す
return x["data2"].std() > 3
print(df.groupby("key")["data2"].std())
print(df.groupby("key").filter(filter_func))
# transform() は元の行数を保ったまま返す
print(df.groupby("key").transform(lambda x: x - x.mean()))
print(df.groupby("key").apply(lambda x: x - x.mean()))
def norm_by_data2(x):
# グループ内で data2 の合計を使って正規化する
x = x.copy()
x["data1"] /= x["data2"].sum()
return x
print(df.groupby("key").apply(norm_by_data2))
グループキーを工夫する
groupby() のキーは列名だけではありません。リスト、辞書、関数、複数キーの組み合わせも使えます。
import numpy as np
import pandas as pd
df = pd.DataFrame({
"key": ["A", "B", "C", "C", "B", "A"],
"data1": range(6),
"data2": np.random.randint(0, 10, size=6),
})
L = [0, 1, 0, 1, 2, 0]
print(df)
# リストをグループキーとして使う
print(df.groupby(L).sum())
df2 = df.set_index("key")
mapping = {"A": "first", "B": "constant", "C": "constant"}
print(df2)
# 辞書や関数もグループキーにできる
print(df2.groupby(mapping).sum())
print(df2.groupby(str.lower).mean())
print(df2.groupby([str.lower, mapping]).mean())
実例 1: 惑星観測データを decade ごとに集計する
実データを使うと、groupby() の意味がかなりつかみやすくなります。ここでは seaborn の惑星データを使い、発見方法と年代ごとに観測数をまとめます。
import seaborn as sns
planets = sns.load_dataset("planets")
# まずはデータの大きさと概要を見る
print(planets.shape)
print(planets.head())
print(planets.describe())
# year から decade 列を作る
decade = 10 * (planets["year"] // 10)
print(decade.head())
decade = decade.astype(str) + "s"
decade.name = "decade"
print(decade.head())
# 発見方法と年代ごとの観測数を集計する
print(planets.groupby(["method", decade]).sum())
print(planets.groupby(["method", decade])[["number"]].sum().unstack().fillna(0))
実例 2: タイタニックデータとピボットテーブル
groupby() と pivot_table() はかなり近いことができます。違いは、pivot_table() の方が集計表として読みやすい形を短く書きやすいことです。
import seaborn as sns
titanic = sns.load_dataset("titanic")
print(titanic.head())
# 年齢を 10 歳刻みへ丸める
T = titanic[titanic.age.notnull()].copy()
Age = 10 * (T["age"] // 10)
Age = Age.astype(int)
print(Age.head())
print((Age.astype(str) + "s").head())
# groupby でもクロス集計の形を作れる
print(T.groupby(["sex", Age])["survived"].mean().unstack())
T.age = Age
# pivot_table() なら同様の集計を短く書ける
print(T.pivot_table("survived", index="sex", columns="age"))
print(titanic.groupby("sex")[["survived"]].mean())
print(titanic.groupby("sex")["survived"].mean())
print(titanic.groupby(["sex", "class"])["survived"].aggregate("mean").unstack())
print(titanic.pivot_table("survived", index="sex", columns="class"))
print(titanic.pivot_table("survived", index="sex", columns="class", aggfunc="mean", margins=True))
print(titanic.pivot_table(index="sex", columns="class", aggfunc={"survived": "sum", "fare": "mean"}))
そのほかの機能
Notebook の最後では、Pandas の応用的な機能がいくつか紹介されています。ここでは見出しだけ出ているものと、実際にコードがあるものを分けて整理します。
文字列操作と時系列処理
この Notebook では「文字列のベクトル化処理」と「時系列処理」という見出しのみが出ており、詳細なコードは続いていません。ただし、Pandas には .str アクセサや日時型の処理機能があり、文字列列や時系列列を一括で扱えるのが大きな特徴です。
多重インデックス
多重インデックスを使うと、都市 x 年 のような二段階のラベルを持つ表を自然に扱えます。複数軸を持つデータを表形式で管理したいときに便利です。
import numpy as np
import pandas as pd
# 都市と年の 2 段階 index を持つ表を作る
base_data = np.array([
[3200, 180000], # 東京 2008
[3340, 200000], # 東京 2018
[1900, 60000], # 大阪 2008
[1950, 70000], # 大阪 2018
[360, 12000], # 横浜 2008
[370, 13000], # 横浜 2018
[700, 50000], # 名古屋 2008
[720, 60000], # 名古屋 2018
])
data = pd.DataFrame(
base_data,
index=[
["東京", "東京", "大阪", "大阪", "横浜", "横浜", "名古屋", "名古屋"],
[2008, 2018] * 4,
],
columns=["population", "GDP"],
)
print(data)
# index 名を付けると意味が分かりやすい
data.index.names = ["city", "year"]
print(data)
# 1 段目だけ / 2 段目まで指定して取り出す
print(data["GDP"])
print(data.loc["大阪", "GDP"])
print(data.loc["大阪", 2018]["GDP"])
eval() と query() による高速化
複雑な式や条件抽出を大量データに対して行うとき、eval() や query() が有効になることがあります。中間配列の生成を減らせるためです。ただし、小さなデータでは普通の書き方の方が速いこともあります。
import numpy as np
import pandas as pd
df1, df2, df3, df4 = (pd.DataFrame(np.random.random((10000, 100))) for _ in range(4))
# 通常の計算式と pd.eval() の速度を比べる
%timeit (df1 + df2) / (df3 + df4)
%timeit pd.eval("(df1+df2)/(df3+df4)")
# 計算結果が一致するか確認する
print(np.allclose((df1 + df2) / (df3 + df4), pd.eval("(df1+df2)/(df3+df4)")))
eval() は列同士の式を DataFrame 内で評価するのにも使えます。@変数名 を使うとローカル変数も参照できます。
import numpy as np
import pandas as pd
df = pd.DataFrame(np.random.random((1000, 3)), columns=list("ABC"))
print(df.head())
# 外部から式を渡して評価する
res_1 = pd.eval("(df.A+df.B)/(df.C-1)")
# DataFrame メソッドとしても書ける
res_2 = df.eval("(A+B)/(C-1)")
print(np.allclose(res_1, res_2))
# 新しい列を計算式から追加する
df["D"] = pd.eval("(df.A+df.B)/(df.C-1)")
print(df.head())
# inplace=True なら元の DataFrame を直接更新する
df.eval("D=(A+B)/(C-1)", inplace=True)
print(df.head())
# @変数名 でローカル変数も参照できる
column_mean = df.mean(axis=1)
res = df.eval("A+@column_mean")
print(res.head())
query() は条件抽出を読みやすく書けるのが利点です。条件式が長くなると、特に見通しがよくなります。
import numpy as np
import pandas as pd
df = pd.DataFrame(np.random.random((1000, 3)), columns=list("ABC"))
print(df.head())
# 通常の条件式と query() の速度を比べる
%timeit df[(df.A < 0.5) & (df.B > 0.5)]
%timeit df.query("(A < 0.5) & (B > 0.5)")
# query() の結果をそのまま確認する
print(df.query("(A < 0.5) & (B > 0.5)").head())
print(np.allclose(df[(df.A < 0.5) & (df.B > 0.5)], df.query("(A < 0.5) & (B > 0.5)")))
# メモリ使用量の比較用
print(df.values.nbytes)
print(df1.values.nbytes)
まとめ
Pandas を最初に学ぶときは、関数名を単発で覚えるよりも、次の流れでつなげて覚えると理解しやすくなります。
-
SeriesとDataFrameを作り、Pandas の基本的な器を理解する。 -
index、columns、loc、ilocで表を読む。 -
head()、info()、統計量でデータの全体像を確認する。 -
isnull()、dropna()、fillna()で欠損値を扱う。 -
concat()とmerge()で表を結合する。 -
groupby()とpivot_table()で集計表を作る。 - 必要に応じて
eval()やquery()で式を簡潔かつ効率的に書く。
この流れで何度か手を動かすと、CSV の読み込みから前処理、集計、可視化前の整形までを、Pandas を軸に自然につなげられるようになります。