分割・適用・結合
多くの問いはグループごとに1つの数を求めます。セクターごとのエクスポージャー、戦略ごとの平均リターン、日ごとの約定数などです。パターンは常に同じ分割・適用・結合(split-apply-combine)です。df.groupby("sector") は列の値で行をグループに分割し、.sum() などの集計を各グループに適用し、pandas がその結果をグループのキーでインデックスされた1つの表に結合します。これがなければ合計を溜める辞書を使ったループを書くことになりますが、これなら問い全体が1行で済みます:df.groupby("sector")["value"].sum()。
import pandas as pd
df = pd.DataFrame({
"symbol": ["AAPL", "MSFT", "GOOG", "TSLA", "F"],
"sector": ["tech", "tech", "tech", "auto", "auto"],
"value": [36850.0, 20510.0, 51300.0, 30120.0, 9000.0],
"ret": [0.012, -0.004, 0.021, 0.03, -0.01],
})
print(df.groupby("sector")["value"].sum())
# sector
# auto 39120.0
# tech 108660.0
# Name: value, dtype: float64
print(df.groupby("sector").size().to_dict()) # {'auto': 2, 'tech': 3} rows per group
print(df.groupby("sector")["ret"].mean().round(4).to_dict()) # {'auto': 0.01, 'tech': 0.0097}agg で複数の要約を一度に
.agg() は複数の要約を1回で計算し、結果の列に名前を付けられます:df.groupby("sector").agg(count=("value", "size"), mean_return=("ret", "mean"), max_value=("value", "max"))。各キーワードが新しい列名で、タプルは読む列と適用する関数("sum"・"mean"・"min"・"max"・"std"・"size"・"first" など)を表します。groupby(["sector", "day"]) のように複数列で同時にグループ化すると、2階層のインデックスの結果になります。グループのキーは既定でソートされ、キーが欠損(NaN)の行は除かれます。
import pandas as pd
df = pd.DataFrame({
"symbol": ["AAPL", "MSFT", "GOOG", "TSLA", "F"],
"sector": ["tech", "tech", "tech", "auto", "auto"],
"value": [36850.0, 20510.0, 51300.0, 30120.0, 9000.0],
"ret": [0.012, -0.004, 0.021, 0.03, -0.01],
})
summary = df.groupby("sector").agg(
count=("value", "size"),
mean_return=("ret", "mean"),
max_value=("value", "max"),
)
print(summary)
# count mean_return max_value
# sector
# auto 2 0.010000 30120.0
# tech 3 0.009667 51300.0
print(df.groupby("sector")["ret"].agg(["min", "max"])) # a list of function names
# min max
# sector
# auto -0.010 0.030
# tech -0.004 0.021transform:各行にグループの統計量を付ける
集計は各グループを1行に縮めます。transform は元の形を保ちます。グループの統計量を計算し、グループの各行に繰り返して、元のインデックスに揃えます。「行の値をそのグループと比べる」ための道具で、ポジションのセクター内シェアは df["value"] / df.groupby("sector")["value"].transform("sum")、グループ内の平均除去は x - x.groupby(g).transform("mean") です。各セクターで最も成績の良い銘柄のように、グループごとに行全体を選ぶには、idxmax() で各グループの最大値のインデックスラベルを得て、.loc でその行を選びます。
import pandas as pd
df = pd.DataFrame({
"symbol": ["AAPL", "MSFT", "GOOG", "TSLA", "F"],
"sector": ["tech", "tech", "tech", "auto", "auto"],
"value": [36850.0, 20510.0, 51300.0, 30120.0, 9000.0],
"ret": [0.012, -0.004, 0.021, 0.03, -0.01],
})
totals = df.groupby("sector")["value"].transform("sum") # one value per original row
print(totals.tolist()) # [108660.0, 108660.0, 108660.0, 39120.0, 39120.0]
print((df["value"] / totals).round(3).tolist()) # [0.339, 0.189, 0.472, 0.77, 0.23]
best = df.loc[df.groupby("sector")["ret"].idxmax()].set_index("sector")[["symbol", "ret"]]
print(best)
# symbol ret
# sector
# auto TSLA 0.030
# tech GOOG 0.021import pandas as pd
t = pd.DataFrame({
"day": ["mon", "mon", "tue", "tue"],
"sector": ["tech", "auto", "tech", "tech"],
"value": [1.0, 2.0, 3.0, 4.0],
})
# Two keys give a two-level index
print(t.groupby(["day", "sector"])["value"].sum())
# day sector
# mon auto 2.0
# tech 1.0
# tue tech 7.0
# Name: value, dtype: float64
# reset_index() turns the keys back into ordinary columns
print(t.groupby(["day", "sector"])["value"].sum().reset_index())
# day sector value
# 0 mon auto 2.0
# 1 mon tech 1.0
# 2 tue tech 7.0for name, group in df.groupby("sector"): でループもできますが、これは少数のグループを表示・描画するのに向いており、数値計算では組み込みの集計と transform のほうが Python のループや関数を渡す apply より短く、はるかに高速です。結果のインデックス(グループのキー、通常はソート済み)と dtype を確認し、size は行数を、count は欠損でない値の数を数えることを覚えておきましょう。df.groupby(キー)[col].sum()(mean・max・sizeなども):グループごとに1つの値で、キーがインデックス.agg(名前=(列, 関数), ...)で名前付きの複数の要約を一度に作るtransformは行数を保つ(シェア・平均除去)。idxmax+.locでグループごとの行全体を選ぶ
次は表の結合です。価格と参照データのように、キーを共有する表を組み合わせます。
ラボ:採点される内容
total_by_sectorでセクターごとのエクスポージャーを合計する25点summary_by_sectorで全セクターを1つの表に要約する25点share_of_sectorで各ポジションをセクター内で比較する25点best_per_sectorでセクターごとの最良銘柄を選ぶ25点