読者です 読者をやめる 読者になる 読者になる

StatsFragments

Python, R, Rust, 統計, 機械学習とか

Python pandas データのイテレーションと関数適用、pipe

pandas ではデータを 列 や 表形式のデータ構造として扱うが、これらのデータから順番に値を取得 (イテレーション) して何か操作をしたい / また 何らかの関数を適用したい、ということがよくある。このエントリでは以下の 3 つについて整理したい。

それぞれ、SeriesDataFrameGroupBy (DataFrame.groupbyしたデータ) で可能な操作が異なるため、順に記載する。

まずは必要なパッケージを import する。

import numpy as np
import pandas as pd

イテレーション

Series

Series は以下 2つのイテレーションメソッドを持つ。各メソッドの挙動は以下のようになる。

図で表すとこんな感じ。矢印が処理の方向、枠内が 1 処理単位。

f:id:sinhrks:20150618213404p:plain

s = pd.Series([1, 2, 3], index=['a', 'b', 'c'])

for v in s:
    print(v)
# 1
# 2
# 3

for i, v in s.iteritems():
    print(i)
    print(v)
    print('')
# a
# 1
# 
# b
# 2
# 
# c
# 3

DataFrame

DataFrame は以下 3つのイテレーションメソッドを持つ。同様に挙動を示す。

f:id:sinhrks:20150618222231p:plain

df = pd.DataFrame({'A': [1, 2, 3], 'B': [4, 5, 6]}, index=['a', 'b', 'c'])
df
#    A  B
# a  1  4
# b  2  5
# c  3  6

for col in df:
    print(col)
# A
# B

for key, column in df.iteritems():
    print(key)
    print(column)
    print('')
# A
# a    1
# b    2
# c    3
# Name: A, dtype: int64
# 
# B
# a    4
# b    5
# c    6
# Name: B, dtype: int64

for key, row in df.iterrows():
    print(key)
    print(row)
    print('')
# a
# A    1
# B    4
# Name: a, dtype: int64
# 
# b
# A    2
# B    5
# Name: b, dtype: int64
# 
# c
# A    3
# B    6
# Name: c, dtype: int64

GroupBy

GroupBy は以下のイテレーションメソッドを持つ。

  • __iter__: GroupBy のグループ名と グループ ( DataFrame もしくは Series ) からなる tupleイテレーション

f:id:sinhrks:20150618213434p:plain

df = pd.DataFrame({'group': ['g1', 'g2', 'g1', 'g2'],
                   'A': [1, 2, 3, 4], 'B': [5, 6, 7, 8]}, 
                  columns=['group', 'A', 'B'])
df
#   group  A  B
# 0    g1  1  5
# 1    g2  2  6
# 2    g1  3  7
# 3    g2  4  8

grouped = df.groupby('group')

for name, group in grouped:
    print(name)
    print(group)
    print('')
# g1
#   group  A  B
# 0    g1  1  5
# 2    g1  3  7
# 
# g2
#   group  A  B
# 1    g2  2  6
# 3    g2  4  8

関数適用

Series

Series の各値に対して 関数を適用する方法は以下の 2 つ。挙動はほぼ一緒だが、関数適用する場合は apply を使ったほうが意図が明確だと思う

  • Series.apply: Series の各値に対して関数を適用。
  • Series.map: Series の各値を、引数を用いてマッピングする。引数として、dictSeries も取れる。

f:id:sinhrks:20150618213448p:plain

s = pd.Series([1, 2, 3], index=['a', 'b', 'c'])

s.apply(lambda x: x * 2)
# a    2
# b    4
# c    6
# dtype: int64

# apply の引数には、Series の値そのものが渡っている
s.apply(type)
# a    <type 'numpy.int64'>
# b    <type 'numpy.int64'>
# c    <type 'numpy.int64'>
# dtype: object

# 関数が複数の返り値を返す場合、結果は tuple の Series になる
s.apply(lambda x: (x, x * 2))
# a    (1, 2)
# b    (2, 4)
# c    (3, 6)
# dtype: object

# 結果を DataFrame にしたい場合は、返り値を Series として返す
s.apply(lambda x: pd.Series([x, x * 2], index=['col1', 'col2']))
#    col1  col2
# a     1     2
# b     2     4
# c     3     6

# map の挙動は apply とほぼ同じ (map では結果を DataFrame にすることはできない)
s.map(lambda x: x * 2)
# a    2
# b    4
# c    6
# dtype: int64

s.map(type)
# a    <type 'numpy.int64'>
# b    <type 'numpy.int64'>
# c    <type 'numpy.int64'>
# dtype: object

# map は 関数以外に、 mapping 用の dict や Series を引数として取れる
s.map(pd.Series(['x', 'y', 'z'], index=[1, 2, 3]))
# a    x
# b    y
# c    z
# dtype: object

DataFrame

DataFrame に対して 関数を適用する方法は以下の 2 つ。

  • DataFrame.apply: DataFrame の各列もしくは各行に対して関数を適用。行 / 列の指定は axis キーワードで行う。
  • DataFrame.applymap: DataFrame の各値に対して関数を適用。

f:id:sinhrks:20150618213500p:plain

df = pd.DataFrame({'A': [1, 2, 3], 'B': [4, 5, 6]}, index=['a', 'b', 'c'])
df
#    A  B
# a  1  4
# b  2  5
# c  3  6

# 各列に対して関数適用
df.apply(lambda x: np.sum(x))
A     6
B    15
dtype: int64

# 各行に対して関数適用
df.apply(lambda x: np.sum(x), axis=1)
a    5
b    7
c    9
dtype: int64

# 各値に対して関数適用
df.applymap(lambda x: x * 2)
#    A   B
# a  2   8
# b  4  10
# c  6  12

# apply で適用される関数には、各列もしくは各行が Series として渡される
df.apply(type)
# A    <class 'pandas.core.series.Series'>
# B    <class 'pandas.core.series.Series'>
# dtype: object

# applymap で適用される関数には、値そのものが引数として渡される
df.applymap(type)
#                       A                     B
# a  <type 'numpy.int64'>  <type 'numpy.int64'>
# b  <type 'numpy.int64'>  <type 'numpy.int64'>
# c  <type 'numpy.int64'>  <type 'numpy.int64'>

GroupBy

GroupBy については、GroupBy.apply で各グループに関数を適用できる。

f:id:sinhrks:20150618213517p:plain

df = pd.DataFrame({'group': ['g1', 'g2', 'g1', 'g2'],
                   'A': [1, 2, 3, 4], 'B': [5, 6, 7, 8]}, 
                  columns=['group', 'A', 'B'])
df
#   group  A  B
# 0    g1  1  5
# 1    g2  2  6
# 2    g1  3  7
# 3    g2  4  8

grouped = df.groupby('group')

grouped.apply(np.mean)
#        A  B
# group      
# g1     2  6
# g2     3  7

補足 処理最適化のため、対象となるグループの数 == 関数適用の実行回数とはならないので注意。関数中で破壊的な処理を行うと意図しない結果になりうる。

# 適用される関数
def f(x):
    print('called')
    return x

# グループ数は 2
grouped.ngroups
# 2

# f の実行は 3 回
grouped.apply(f)
# called
# called
# called

pipe

先日 リリースされた v0.16.2 にて pipe メソッドが追加された。これは R の {magrittr} というパッケージからインスパイアされたもので、データへの連続した操作を メソッドチェイン (複数メソッドの連続した呼び出し) で記述することを可能にする。

Series.pipeDataFrame.pipe それぞれ、x.pipe(f, *args, **kwargs)f(x, *args, **kwargs) と同じ。つまり、データ全体に対する関数適用になる。

f:id:sinhrks:20150618213530p:plain

補足 GroupBy.pipe は v0.16.2 時点では存在しない。

# 渡される型は呼び出し元のインスタンス
s.pipe(type)
# pandas.core.series.Series

df.pipe(type)
# pandas.core.frame.DataFrame

np.random.seed(1)
df = pd.DataFrame(np.random.randn(10, 10))

# DataFrame を引数として heatmap を描く関数を定義
def heatmap(df):
    import matplotlib.pyplot as plt
    fig, ax = plt.subplots()
    return ax.pcolor(df.values, cmap='Greens')

# heatmap(df) と同じ。
df.pipe(heatmap)

f:id:sinhrks:20150618221027p:plain

まとめ

イテレーション、関数適用、pipe について整理した。特に関数適用は データの前処理時に頻出するため、パターンを覚えておくと便利。

Pythonによるデータ分析入門 ―NumPy、pandasを使ったデータ処理

Pythonによるデータ分析入門 ―NumPy、pandasを使ったデータ処理