Skip to content

100 pandas 練習問題(日本語訳)

このページは、Alex Riley 氏による 100 pandas puzzles の日本語訳です。第 5 回「データ操作の基礎 - pandas」の補助教材として、自学自習用にご利用ください。

出典・ライセンス

  • 原典: ajcr/100-pandas-puzzles
  • 著作権: Copyright (c) Alex Riley
  • ライセンス: MIT License
  • 翻訳: 茨城大学「データ活用法」講義(非公式日本語訳)
  • 翻訳方法: 本ページの日本語訳は AI(大規模言語モデル)を用いて作成しています。誤訳や不自然な表現が含まれる可能性があるため、疑問がある場合は原典を参照してください。

問題数について

原典のタイトルは「100 pandas puzzles」ですが、現時点で公開されている問題は 60 問 までです(最後に "More exercises to follow soon..." と記載があります)。本ページもそれに従って 60 問を収録しています。

使い方

問題は基本操作から応用までセクションに分かれ、各セクションに難易度が設定されています。各問題には 「解答」 のアコーディオンがあり、クリックすると展開できます。

  1. まず問題文だけを読んで、自分で書いてみる
  2. 詰まったら解答を開いて確認する

すべての問題に取り組む必要はありません。気になるものから挑戦してみてください。

準備
import pandas as pd
import numpy as np

A. pandas のインポート(難易度:簡単)

1. pandas を pd というエイリアスでインポートしてください

解答
import pandas as pd

2. インポートした pandas のバージョンを表示してください

解答
pd.__version__

3. pandas が依存している各種ライブラリのバージョン情報をすべて表示してください

解答
pd.show_versions()

B. DataFrame の基本操作(難易度:簡単)

DataFrame に対する「選択」「ソート」「追加」「集計」の基本操作です。NumPy も使うため、以下のようにインポートしておきます。

import numpy as np

ここからの問題では、次の Python 辞書 data とリスト labels を使用します。

data = {'animal': ['cat', 'cat', 'snake', 'dog', 'dog', 'cat', 'snake', 'cat', 'dog', 'dog'],
        'age': [2.5, 3, 0.5, np.nan, 5, 2, 4.5, np.nan, 7, 3],
        'visits': [1, 3, 2, 3, 2, 3, 1, 1, 2, 1],
        'priority': ['yes', 'yes', 'no', 'yes', 'no', 'no', 'no', 'yes', 'no', 'no']}

labels = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j']

(動物と動物病院への通院記録をテーマにした架空のデータです。)

4. この data 辞書から DataFrame df を作成し、インデックスを labels にしてください

解答
import numpy as np

data = {'animal': ['cat', 'cat', 'snake', 'dog', 'dog', 'cat', 'snake', 'cat', 'dog', 'dog'],
        'age': [2.5, 3, 0.5, np.nan, 5, 2, 4.5, np.nan, 7, 3],
        'visits': [1, 3, 2, 3, 2, 3, 1, 1, 2, 1],
        'priority': ['yes', 'yes', 'no', 'yes', 'no', 'no', 'no', 'yes', 'no', 'no']}

labels = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j']

df = pd.DataFrame(data, index=labels)

5. この DataFrame の基本的な情報の要約を表示してください(ヒント:DataFrame の単一メソッドで実現できます)

解答
df.info()

# ...または...

df.describe()

6. DataFrame df の最初の 3 行を表示してください

解答
df.iloc[:3]

# または同等の書き方
df.head(3)

7. DataFrame df から 'animal' と 'age' の列だけを選択してください

解答
df.loc[:, ['animal', 'age']]

# または
df[['animal', 'age']]

8. 行 [3, 4, 8] のうち、列 ['animal', 'age'] のデータを選択してください

解答
df.loc[df.index[[3, 4, 8]], ['animal', 'age']]

9. visits が 3 より大きい行だけを選択してください

解答
df[df['visits'] > 3]

10. age が欠損(NaN)の行を選択してください

解答
df[df['age'].isnull()]

11. animal が cat かつ age が 3 未満の行を選択してください

解答
df[(df['animal'] == 'cat') & (df['age'] < 3)]

12. age が 2 以上 4 以下の行を選択してください

解答
df[df['age'].between(2, 4)]

13. 行 'f' の age を 1.5 に変更してください

解答
df.loc['f', 'age'] = 1.5

14. df のすべての visits の合計(つまり通院回数の総計)を求めてください

解答
df['visits'].sum()

15. df の各 animal ごとの age の平均を求めてください

解答
df.groupby('animal')['age'].mean()

16. 任意の値を持つ新しい行 'k' を df に追加してください。その後、その行を削除して元の DataFrame に戻してください

解答
df.loc['k'] = [5.5, 'dog', 'no', 2]

# その後、新しい行を削除...
df = df.drop('k')

17. df の各 animal の出現回数を数えてください

解答
df['animal'].value_counts()

18. df をまず 'age' の値で降順にソートし、次に 'visits' の値で昇順にソートしてください(結果として行 i が先頭、行 d が末尾になります)

解答
df.sort_values(by=['age', 'visits'], ascending=[False, True])

19. 'priority' 列には 'yes' と 'no' の値が含まれています。この列を、'yes' を True、'no' を False とする真偽値の列に置き換えてください

解答
df['priority'] = df['priority'].map({'yes': True, 'no': False})

20. 'animal' 列の 'snake' を 'python' に変更してください

解答
df['animal'] = df['animal'].replace('snake', 'python')

21. 各 animal、各 visits の組み合わせごとに age の平均を求めてください。つまり、行が animal、列が visits、値が age の平均となるようにしてください(ヒント:pivot table を使います)

解答
df.pivot_table(index='animal', columns='visits', values='age', aggfunc='mean')

C. DataFrame:基本を超えて(難易度:中級)

少し難しめの問題です。1 つのメソッドでは解けず、複数のメソッドを組み合わせる必要があるものもあります。

22. 整数の列 'A' を持つ DataFrame df があります。次のような DataFrame からスタートします

df = pd.DataFrame({'A': [1, 2, 2, 3, 4, 5, 5, 5, 6, 7, 7]})

このとき、すぐ上の行と同じ整数を含む行を取り除く方法は?

結果として、次の値を持つ列が残るはずです:

1, 2, 3, 4, 5, 6, 7
解答
df = pd.DataFrame({'A': [1, 2, 2, 3, 4, 5, 5, 5, 6, 7, 7]})

df.loc[df['A'].shift() != df['A']]

# 別解として drop_duplicates() を使う方法もある。ただし、
# これは *すべての* 重複を除去するため、A が [1, 1, 2, 2, 1, 1]
# のような場合は意図通りに動作しないので注意。
df.drop_duplicates(subset='A')

23. ランダムな数値で構成される DataFrame があります

df = pd.DataFrame(np.random.random(size=(5, 3)))  # 5x3 の float DataFrame

各行の平均値を、その行のそれぞれの要素から引くにはどうしますか?

解答
df = pd.DataFrame(np.random.random(size=(5, 3)))

df.sub(df.mean(axis=1), axis=0)

24. 実数値からなる 10 列の DataFrame があります

df = pd.DataFrame(np.random.random(size=(5, 10)), columns=list('abcdefghij'))

各列の合計が最も小さい列はどれですか?その列のラベルを返してください

解答
df = pd.DataFrame(np.random.random(size=(5, 10)), columns=list('abcdefghij'))

df.sum().idxmin()

25. DataFrame に重複しないユニークな行が何行あるかを数えるにはどうしますか?(重複している行はすべて無視)

解答
df = pd.DataFrame(np.random.randint(0, 2, size=(10, 3)))

len(df) - df.duplicated(keep=False).sum()

# またはよりシンプルに...
len(df.drop_duplicates(keep=False))

次の 3 問は少し難しめです。

26. 以下のセルにある DataFrame df は、浮動小数点数の 10 列で構成されており、各行にはちょうど 5 つの NaN 値が含まれています

DataFrame の各行について、3 つ目の NaN 値が含まれる列を見つけてください。

結果は列ラベルの Series として返してください:e, c, d, h, d

解答
nan = np.nan

data = [[0.04,  nan,  nan, 0.25,  nan, 0.43, 0.71, 0.51,  nan,  nan],
        [ nan,  nan,  nan, 0.04, 0.76,  nan,  nan, 0.67, 0.76, 0.16],
        [ nan,  nan, 0.5 ,  nan, 0.31, 0.4 ,  nan,  nan, 0.24, 0.01],
        [0.49,  nan,  nan, 0.62, 0.73, 0.26, 0.85,  nan,  nan,  nan],
        [ nan,  nan, 0.41,  nan, 0.05,  nan, 0.61,  nan, 0.48, 0.68]]

columns = list('abcdefghij')

df = pd.DataFrame(data, columns=columns)

(df.isnull().cumsum(axis=1) == 3).idxmax(axis=1)

27. グループの列 'grps' と整数値の列 'vals' を持つ DataFrame があります

df = pd.DataFrame({'grps': list('aaabbcaabcccbbc'),
                   'vals': [12, 345, 3, 1, 45, 14, 4, 52, 54, 23, 235, 21, 57, 3, 87]})

グループごとに上位 3 つの値の合計を求めてください。結果は以下のようになるはずです:

grps
a    409
b    156
c    345
解答
df = pd.DataFrame({'grps': list('aaabbcaabcccbbc'),
                   'vals': [12, 345, 3, 1, 45, 14, 4, 52, 54, 23, 235, 21, 57, 3, 87]})

df.groupby('grps')['vals'].nlargest(3).sum(level=0)

28. 下のコードで構成される DataFrame df には、2 つの整数列 'A' と 'B' があります。'A' の値は 1〜100(両端を含む)です

'A' の 10 ずつの連続した区間(つまり (0, 10], (10, 20], ...)ごとに、対応する 'B' の値の合計を求めてください。

結果は以下のような Series になります:

A
(0, 10]      635
(10, 20]     360
(20, 30]     315
(30, 40]     306
(40, 50]     750
(50, 60]     284
(60, 70]     424
(70, 80]     526
(80, 90]     835
(90, 100]    852
解答
df = pd.DataFrame(np.random.RandomState(8765).randint(1, 101, size=(100, 2)), columns=["A", "B"])

df.groupby(pd.cut(df['A'], np.arange(0, 101, 10)))['B'].sum()

D. DataFrame:難しい問題(難易度:難)

少し発想を変える必要があるかもしれません……が、すべて通常の pandas / NumPy のメソッドだけで解けます(明示的な for ループを使わずに)。

29. 整数列 'X' を持つ DataFrame df を考えます

df = pd.DataFrame({'X': [7, 2, 0, 3, 4, 2, 5, 0, 3, 4]})

各値について、直前のゼロまでの距離(または Series の先頭までの距離、近い方)を数えてください。結果は次のようになります:

[1, 2, 0, 1, 2, 3, 4, 0, 1, 2]

これを新しい列 'Y' として追加してください。

解答
df = pd.DataFrame({'X': [7, 2, 0, 3, 4, 2, 5, 0, 3, 4]})

izero = np.r_[-1, (df == 0).values.nonzero()[0]]  # ゼロのインデックス
idx = np.arange(len(df))
y = df['X'] != 0
df['Y'] = idx - izero[np.searchsorted(izero - 1, idx) - 1]

# http://stackoverflow.com/questions/30730981/
# credit: Behzad Nouri

pandas cookbook に基づく別解:

df = pd.DataFrame({'X': [7, 2, 0, 3, 4, 2, 5, 0, 3, 4]})

x = (df['X'] != 0).cumsum()
y = x != x.shift()
df['Y'] = y.groupby((y != y.shift()).cumsum()).cumsum()

groupby を使うさらに別の方法:

df = pd.DataFrame({'X': [7, 2, 0, 3, 4, 2, 5, 0, 3, 4]})

df['Y'] = df.groupby((df['X'] == 0).cumsum()).cumcount()

# 最初のゼロに到達する前は 1 つずれているので補正
first_zero_idx = (df['X'] == 0).idxmax()
df['Y'].iloc[0:first_zero_idx] += 1

30. 以下のコードで構成される DataFrame には、数値データの行と列が含まれます

この DataFrame の中で値が最も大きい 3 つの位置(列, 行)のインデックスのリストを作成してください。今回の場合の答えは:

[(5, 7), (6, 4), (2, 5)]
解答
df = pd.DataFrame(np.random.RandomState(30).randint(1, 101, size=(8, 8)))

df.unstack().sort_values()[-3:].index.tolist()

# http://stackoverflow.com/questions/14941261/
# credit: DSM

31. 以下に、グループ ID の列 'grps' と、対応する整数値の列 'vals' を持つ DataFrame があります

df = pd.DataFrame({"vals": np.random.RandomState(31).randint(-30, 30, size=15),
                   "grps": np.random.RandomState(31).choice(["A", "B"], 15)})

新しい列 'patched_vals' を作成し、'vals' の値をそのままコピーしつつ、負の値はその所属グループの平均値に置き換えてください:

    vals grps  patched_vals
0    -12    A          13.6
1     -7    B          28.0
2    -14    A          13.6
3      4    A           4.0
4     -7    A          13.6
5     28    B          28.0
6     -2    A          13.6
7     -1    A          13.6
8      8    A           8.0
9     -2    B          28.0
10    28    A          28.0
11    12    A          12.0
12    16    A          16.0
13   -24    A          13.6
14   -12    A          13.6
解答
df = pd.DataFrame({"vals": np.random.RandomState(31).randint(-30, 30, size=15),
                   "grps": np.random.RandomState(31).choice(["A", "B"], 15)})

def replace(group):
    mask = group < 0
    group[mask] = group[~mask].mean()
    return group

df.groupby(['grps'])['vals'].transform(replace)

# http://stackoverflow.com/questions/14760757/
# credit: unutbu

32. NaN 値を無視する、ウィンドウサイズ 3 のグループ別移動平均を実装してください。例えば次の DataFrame を考えます

df = pd.DataFrame({'group': list('aabbabbbabab'),
                   'value': [1, 2, 3, np.nan, 2, 3, np.nan, 1, 7, 3, np.nan, 8]})

目標は、次の Series を計算することです:

0     1.000000
1     1.500000
2     3.000000
3     3.000000
4     1.666667
5     3.000000
6     3.000000
7     2.000000
8     3.666667
9     2.000000
10    4.500000
11    4.000000

例えば、グループ 'b' の最初のウィンドウサイズ 3 は値 3.0, NaN, 3.0 を持ち、これは行インデックス 5 で発生します。新しい列の値は NaN ではなく 3.0 となるべきです(2 つの非 NaN 値だけで平均 (3+3)/2 を計算)。

解答
df = pd.DataFrame({'group': list('aabbabbbabab'),
                   'value': [1, 2, 3, np.nan, 2, 3, np.nan, 1, 7, 3, np.nan, 8]})

g1 = df.groupby(['group'])['value']              # グループ別の値
g2 = df.fillna(0).groupby(['group'])['value']    # NaN を 0 で埋めてからグループ別

s = g2.rolling(3, min_periods=1).sum() / g1.rolling(3, min_periods=1).count()  # 平均を計算

s.reset_index(level=0, drop=True).sort_index()

E. Series と DatetimeIndex(難易度:簡単〜中級)

datetime データを含む Series の作成と操作の練習です。pandas は日付・時刻の取り扱いに優れています。

33. 2015 年の各営業日を含む DatetimeIndex を作成し、それをインデックスとしてランダムな値の Series を作成してください。この Series を s と呼びます

解答
dti = pd.date_range(start='2015-01-01', end='2015-12-31', freq='B')
s = pd.Series(np.random.rand(len(dti)), index=dti)
s

34. s のうち、すべての水曜日の値の合計を求めてください

解答
s[s.index.weekday == 2].sum()

35. s の各月ごとの値の平均を求めてください

解答
s.resample('M').mean()

36. s の連続する 4 ヶ月ごとに、最大値が発生した日付を求めてください

解答
s.groupby(pd.Grouper(freq='4M')).idxmax()

37. 2015 年と 2016 年の各月の第 3 木曜日からなる DateTimeIndex を作成してください

解答
pd.date_range('2015-01-01', '2016-12-31', freq='WOM-3THU')

F. データのクリーニング(難易度:簡単〜中級)

DataFrame を扱いやすい形に整える練習です。「文字列が崩れていて、リストや欠損値が混じったデータをもらってしまった、どうやって整えよう?」という場面はよくあります。

このセクションでは、次のような「悲惨な」DataFrame を使います:

df = pd.DataFrame({'From_To': ['LoNDon_paris', 'MAdrid_miLAN', 'londON_StockhOlm',
                               'Budapest_PaRis', 'Brussels_londOn'],
                   'FlightNumber': [10045, np.nan, 10065, np.nan, 10085],
                   'RecentDelays': [[23, 47], [], [24, 43, 87], [13], [67, 32]],
                   'Airline': ['KLM(!)', '<Air France> (12)', '(British Airways. )',
                               '12. Air France', '"Swiss Air"']})

整形すると以下のようになります:

            From_To  FlightNumber  RecentDelays              Airline
0      LoNDon_paris       10045.0      [23, 47]               KLM(!)
1      MAdrid_miLAN           NaN            []    <Air France> (12)
2  londON_StockhOlm       10065.0  [24, 43, 87]  (British Airways. )
3    Budapest_PaRis           NaN          [13]       12. Air France
4   Brussels_londOn       10085.0      [67, 32]          "Swiss Air"

(架空のフライトデータです。実際のものを意図したものではありません。)

38. FlightNumber 列の一部の値が欠損(NaN)しています。これらの番号は各行で 10 ずつ増える設計なので、10055 と 10075 が補完されるべきです。df を変更してこれらの欠損値を埋め、さらに列を整数型(float ではなく int)にしてください

解答
df = pd.DataFrame({'From_To': ['LoNDon_paris', 'MAdrid_miLAN', 'londON_StockhOlm',
                               'Budapest_PaRis', 'Brussels_londOn'],
                   'FlightNumber': [10045, np.nan, 10065, np.nan, 10085],
                   'RecentDelays': [[23, 47], [], [24, 43, 87], [13], [67, 32]],
                   'Airline': ['KLM(!)', '<Air France> (12)', '(British Airways. )',
                               '12. Air France', '"Swiss Air"']})

df['FlightNumber'] = df['FlightNumber'].interpolate().astype(int)
df

39. From_To 列は、2 つの別々の列に分けた方がよさそうです。各文字列をアンダースコア _ で区切り、正しい値を持つ一時的な DataFrame temp を作ってください。さらに、列名を 'From' と 'To' に正しく設定してください

解答
temp = df.From_To.str.split('_', expand=True)
temp.columns = ['From', 'To']
temp

40. 一時的な DataFrame temp の都市名は大文字と小文字が入り乱れています。最初の文字だけ大文字、それ以外は小文字となるように標準化してください(例:「londON」→「London」)

解答
temp['From'] = temp['From'].str.capitalize()
temp['To'] = temp['To'].str.capitalize()
temp

41. df から From_To 列を削除し、前の問題で作った一時的な DataFrame temp を結合してください

解答
df = df.drop('From_To', axis=1)
df = df.join(temp)
df

42. Airline 列を見ると、航空会社名の周りに余分な記号や記法が付いています。航空会社名だけを取り出してください(例:'(British Airways. )' → 'British Airways')

解答
df['Airline'] = df['Airline'].str.extract('([a-zA-Z\s]+)', expand=False).str.strip()
# 注: .strip() で前後の空白も除去
df

43. RecentDelays 列の値はリストとして格納されています。各リストの 1 番目の値を独立した列、2 番目の値を別の独立した列……というように展開したいです。N 番目の値がない場合は NaN とします

リスト型の Series を新しい DataFrame delays に展開し、列名を 'delay_1', 'delay_2', ... と変更し、df の元の RecentDelays 列を 'delays' で置き換えてください。

クリーニング後の DataFrame は以下のようになるはずです:

   FlightNumber          Airline      From         To  delay_1  delay_2  delay_3
0         10045              KLM    London      Paris     23.0     47.0      NaN
1         10055       Air France    Madrid      Milan      NaN      NaN      NaN
2         10065  British Airways    London  Stockholm     24.0     43.0     87.0
3         10075       Air France  Budapest      Paris     13.0      NaN      NaN
4         10085        Swiss Air  Brussels     London     67.0     32.0      NaN
解答
# いくつか方法があるが、以下が最もシンプル

delays = df['RecentDelays'].apply(pd.Series)

delays.columns = ['delay_{}'.format(n) for n in range(1, len(delays.columns) + 1)]

df = df.drop('RecentDelays', axis=1).join(delays)

df

G. MultiIndex の活用(難易度:中級)

これまでは単一インデックスの DataFrame でしたが、pandas は 複数レベル のインデックスもサポートしています。これは Series や DataFrame に新たな次元を追加するようなものです。例えば Series は 1 次元ですが、2 レベルの MultiIndex を使えば 2 次元 DataFrame に近い機能が得られます。

このセクションでは、MultiIndex を使ったデータ分析を練習します。まずは 2 レベルの Series を作るところから始めましょう。

44. リスト letters = ['A', 'B', 'C'] と numbers = list(range(10)) を考えるとき、2 つのリストの直積から MultiIndex オブジェクトを作成し、それをインデックスとしてランダムな数値の Series を作成してください。この Series を s と呼びます

解答
letters = ['A', 'B', 'C']
numbers = list(range(10))

mi = pd.MultiIndex.from_product([letters, numbers])
s = pd.Series(np.random.rand(30), index=mi)
s

45. s のインデックスが辞書式(lexicographically)にソートされているかを確認してください(MultiIndex で正しくインデックス参照を行うために必要な性質です)

解答
s.index.is_lexsorted()

# または冗長に書くと...
s.index.lexsort_depth == s.index.nlevels

46. MultiIndex Series s の第 2 レベルから、ラベル 1、3、6 を選択してください

解答
s.loc[:, [1, 3, 6]]

47. Series s をスライスしてください。第 1 レベルではラベル 'B' まで、第 2 レベルでは 5 以降を取り出します

解答
s.loc[pd.IndexSlice[:'B', 5:]]

# または IndexSlice を使わずに...
s.loc[slice(None, 'B'), slice(5, None)]

48. 第 1 レベルの各ラベルごとに s の値を合計してください(A、B、C ごとの合計の Series が得られるはずです)

解答
s.sum(level=0)

49. もし sum()(やその他のメソッド)が level キーワード引数を受け付けなかったとしたら、s.sum(level=1) と等価な処理をどのように行いますか?

解答
# 一つの方法は .unstack() を使うこと...
# 実は s が「変装した普通の DataFrame」であることがわかる
s.unstack().sum(axis=0)

50. MultiIndex のレベルを入れ替え、(letters, numbers) の形のインデックスにしてください。この新しい Series は辞書式にソートされていますか?されていない場合はソートしてください

解答
new_s = s.swaplevel(0, 1)

if not new_s.index.is_lexsorted():
    new_s = new_s.sort_index()

new_s

H. マインスイーパー(難易度:中級〜難)

マインスイーパー盤面の「安全マス」の数字を生成する練習です。

古いバージョンの Windows でマインスイーパーをやったことがある方も多いのでは? - https://ja.wikipedia.org/wiki/マインスイーパ

ゲームをご存じない方のために:マス目の中に地雷が隠されている盤面を想像してください。地雷をクリックすると即座にゲームオーバー。安全なマスをクリックすると、隣接するマスにある地雷の数が表示されます。目的は地雷のないすべてのマスを開けることです。

このセクションでは、マインスイーパーのゲームに必要なデータ(マスの座標、地雷の有無、隣接地雷数)を持つ DataFrame を作成します。

51. 5×4 のマインスイーパー盤面を考えます

X = 5
Y = 4

まず、この盤面のすべての座標を 'x' と 'y' の 2 列に持つ DataFrame df を作成してください。先頭は次のようになるはずです:

   x  y
0  0  0
1  0  1
2  0  2
...
解答
X = 5
Y = 4

p = pd.core.reshape.util.cartesian_product([np.arange(X), np.arange(Y)])
df = pd.DataFrame(np.asarray(p).T, columns=['x', 'y'])
df

52. この DataFrame df に、ゼロ(安全)か 1(地雷)の新しい列を追加してください。各位置に地雷がある確率は 0.4 です

解答
# 二項分布からサンプリングする方法

df['mine'] = np.random.binomial(1, 0.4, X * Y)
df

53. この DataFrame に 'adjacent' という新しい列を追加してください。この列には、各マスに隣接するマスにある地雷の数を入れます

(例:1 行目はマス (0, 0) のエントリで、(0, 1)、(1, 0)、(1, 1) 上にある地雷を数えます。)

解答
# merge を使った解法(最適ではないが、最初に思いつくアプローチ)

df['adjacent'] = \
    df.merge(df + [ 1,  1, 0], on=['x', 'y'], how='left')\
      .merge(df + [ 1, -1, 0], on=['x', 'y'], how='left')\
      .merge(df + [-1,  1, 0], on=['x', 'y'], how='left')\
      .merge(df + [-1, -1, 0], on=['x', 'y'], how='left')\
      .merge(df + [ 1,  0, 0], on=['x', 'y'], how='left')\
      .merge(df + [-1,  0, 0], on=['x', 'y'], how='left')\
      .merge(df + [ 0,  1, 0], on=['x', 'y'], how='left')\
      .merge(df + [ 0, -1, 0], on=['x', 'y'], how='left')\
       .iloc[:, 3:]\
        .sum(axis=1)

# 別解:DataFrame を pivot して実際の地雷グリッドを作り、畳み込みを使う
# 参考: https://github.com/jakevdp/matplotlib_pydata2013/blob/master/examples/minesweeper.py

from scipy.signal import convolve2d

mine_grid = df.pivot_table(columns='x', index='y', values='mine')
counts = convolve2d(mine_grid.astype(complex), np.ones((3, 3)), mode='same').real.astype(int)
df['adjacent'] = (counts - mine_grid).ravel('F')

54. 地雷を含む行については、'adjacent' 列の値を NaN にしてください

解答
df.loc[df['mine'] == 1, 'adjacent'] = np.nan

55. 最後に、DataFrame を「隣接地雷数」のグリッドに変換してください。列が x 座標、行が y 座標です

解答
df.drop('mine', axis=1).set_index(['y', 'x']).unstack()

I. プロット(難易度:中級)

DataFrame に含まれるデータを本当に理解するためには、可視化が欠かせません。トレンドや異常値がパッと目に飛び込んでくることもよくあります。pandas にはこの機能が組み込まれていて、以下の問題で何ができるかを探っていきます。

56. pandas は matplotlib プロットライブラリと深く統合されており、DataFrame の可視化はとても使いやすいです。ノートブック環境では通常、以下のような決まり文句を使います

import matplotlib.pyplot as plt
%matplotlib inline
plt.style.use('ggplot')

matplotlib は pandas の可視化機能の土台となるライブラリで、通常 plt というエイリアスで使われます。

%matplotlib inline はノートブック内にプロットを表示するための指定です(別ウィンドウを開かない)。

plt.style.use('ggplot') は R の ggplot パッケージのスタイルに基づく、見やすい配色のテーマです。

手始めに、次のランダムデータの散布図を作成してください。ただし、デフォルトのマーカーではなく 黒い X を使ってください。

df = pd.DataFrame({"xs": [1, 5, 2, 8, 1], "ys": [4, 2, 1, 9, 6]})

行き詰まったら 公式ドキュメント を参照してください!

解答
import matplotlib.pyplot as plt
%matplotlib inline
plt.style.use('ggplot')

df = pd.DataFrame({"xs": [1, 5, 2, 8, 1], "ys": [4, 2, 1, 9, 6]})

df.plot.scatter("xs", "ys", color="black", marker="x")

57. DataFrame の列は、色やサイズを変更するためにも使えます。Bill さんは仕事のパフォーマンスを時系列で記録しており、その日の気分、朝コーヒーを飲んだかどうかも一緒に記録しています。この DataFrame の 4 つの特徴すべてを 1 つのプロットに反映させてください

(ヒント:プロットがうまく見えない場合は、size に使う Series を 10 倍以上にしてみてください)

グラフは美しくなくても OK:これは可視化の講義ではないので!

df = pd.DataFrame({"productivity": [5, 2, 3, 1, 4, 5, 6, 7, 8, 3, 4, 8, 9],
                   "hours_in":     [1, 9, 6, 5, 3, 9, 2, 9, 1, 7, 4, 2, 2],
                   "happiness":    [2, 1, 3, 2, 3, 1, 2, 3, 1, 2, 2, 1, 3],
                   "caffienated":  [0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 1, 0]})
解答
df = pd.DataFrame({"productivity": [5, 2, 3, 1, 4, 5, 6, 7, 8, 3, 4, 8, 9],
                   "hours_in":     [1, 9, 6, 5, 3, 9, 2, 9, 1, 7, 4, 2, 2],
                   "happiness":    [2, 1, 3, 2, 3, 1, 2, 3, 1, 2, 2, 1, 3],
                   "caffienated":  [0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 1, 0]})

df.plot.scatter("hours_in", "productivity", s=df.happiness * 30, c=df.caffienated)

58. 複数のグラフを 1 枚にプロットしたい場合は? pandas では matplotlib の Axis オブジェクトを渡せるほか、プロットは Axis オブジェクトを返します

月別の収益を棒グラフで、月別の広告費(百万単位)を折れ線グラフで重ねて描画してください。

df = pd.DataFrame({"revenue":     [57, 68, 63, 71, 72, 90, 80, 62, 59, 51, 47, 52],
                   "advertising": [2.1, 1.9, 2.7, 3.0, 3.6, 3.2, 2.7, 2.4, 1.8, 1.6, 1.3, 1.9],
                   "month": range(12)})
解答
df = pd.DataFrame({"revenue":     [57, 68, 63, 71, 72, 90, 80, 62, 59, 51, 47, 52],
                   "advertising": [2.1, 1.9, 2.7, 3.0, 3.6, 3.2, 2.7, 2.4, 1.8, 1.6, 1.3, 1.9],
                   "month": range(12)})

ax = df.plot.bar("month", "revenue", color="green")
df.plot.line("month", "advertising", secondary_y=True, ax=ax)
ax.set_xlim((-1, 12))

キャンドルスティック(ローソク足)チャート

ここからは、株価データの分析でよく使われる キャンドルスティックチャート(ローソク足チャート)を作ります。キャンドルスティックチャートは、ある時間枠における株価の始値・終値・最高値・最安値を表します。「ろうそく」(バーの太い部分)の色は、終値が始値より高ければ緑、低ければ赤になります。

これはもともと pandas のプロット課題として設計されていましたが、実は pandas のメソッドだけでは描画が現実的でない種類のグラフです。matplotlib に不慣れな場合のために、適切な形式に整形した DataFrame を渡すだけで描画してくれる関数を用意しています。

まず、pandas の時系列グループ化機能を使ってデータを正しい形式に整える必要があります。各ろうそくが 1 時間分のデータを表すようにします。始値・最高値・最安値・終値を返す集約関数を自作することもできますが、pandas にはそれを行うビルトイン機能があります。

下のセルにヘルパー関数が含まれています。day_stock_data() を呼び出すと、架空の株価とその売買時刻を含む DataFrame が生成されます。正しく整形・集約された株価データに対して plot_candlestick(df) を呼ぶと、キャンドルスティックチャートが描画されます。

# このセルのコードは原典そのままを利用してください
import numpy as np

def float_to_time(x):
    return str(int(x)) + ":" + str(int(x % 1 * 60)).zfill(2) + ":" + str(int(x * 60 % 1 * 60)).zfill(2)

def day_stock_data():
    # ニューヨーク証券取引所は 9:30 〜 16:00
    time = 9.5
    price = 100
    results = [(float_to_time(time), price)]
    while time < 16:
        elapsed = np.random.exponential(.001)
        time += elapsed
        if time > 16:
            break
        price_diff = np.random.uniform(.999, 1.001)
        price *= price_diff
        results.append((float_to_time(time), price))

    df = pd.DataFrame(results, columns=['time', 'price'])
    df.time = pd.to_datetime(df.time)
    return df

def plot_candlestick(agg):
    fig, ax = plt.subplots()
    for time in agg.index:
        ax.plot([time.hour] * 2, agg.loc[time, ["high", "low"]].values, color="black")
        ax.plot([time.hour] * 2, agg.loc[time, ["open", "close"]].values,
                color=agg.loc[time, "color"], linewidth=10)

    ax.set_xlim((8, 16))
    ax.set_ylabel("Price")
    ax.set_xlabel("Hour")
    ax.set_title("OHLC of Stock Value During Trading Day")
    plt.show()

59. 1 日分のランダムな株価データを生成し、それを時間単位で集約・整形して、始値・最高値・最安値・終値の時間別サマリーを得てください

解答
df = day_stock_data()
df.head()

df.set_index("time", inplace=True)
agg = df.resample("H").ohlc()
agg.columns = agg.columns.droplevel()
agg["color"] = (agg.close > agg.open).map({True: "green", False: "red"})
agg.head()

60. 適切に整形されたデータが手に入ったので、自分でそれをキャンドルスティックチャートとしてプロットしてみてください。上で定義した plot_candlestick(df) 関数を使うか、行き詰まったら matplotlib の plot ドキュメント を参照してください

解答
plot_candlestick(agg)

以上で公開済みの 60 問の pandas 練習問題の日本語訳は完了です。お疲れさまでした!

原典には More exercises to follow soon...(さらに問題を追加予定)と記載があるので、今後追加される可能性があります。

NumPy の練習問題に取り組みたい場合は、100 NumPy 練習問題 をご覧ください。