12. pandas によるデータ操作
テーマ: 売上データのグループ集計・ピボット・時系列リサンプル・移動平均
学習点: DataFrame, groupby/agg, pivot_table, resample, rolling, merge, メソッドチェーン
依存: NumPy, pandas / 難易度: 中級
実行方法
uv run 12_pandas_pivot.py
スクリプト冒頭の PEP 723 メタデータ(# /// script)により、必要なライブラリは
uv が自動的に仮想環境へ導入します。事前の pip install は不要です。
解説
何をするプログラムか
前章 11 では標準ライブラリだけで売上集計を行いましたが、本章では同種の分析をデータ分析の事実上の標準ツールである pandas で行います。Excel のピボットテーブルで日常的に行われる「地域×チャネルのクロス集計」「月次推移と移動平均」「マスタ表との突合による粗利計算」を、DataFrame のメソッドで表現します。
1,000 件の売上データを乱数で生成し、(1) 地域×商品のグループ集計、(2) ピボットテーブルと構成比、(3) 月次リサンプル・3ヶ月移動平均・前月比・累積、(4) 商品マスタとの結合による粗利率計算、(5) 上位 20% の取引が売上に占めるシェア(パレート分析)、と実務で頻出する操作を一通りたどります。
コードの読みどころ
make_data()ではrng.choice(["東京", ...], n, p=[0.45, ...])で地域の出現確率を指定し、df["product"].map(price)で商品名から単価を引いています。さらにdf.loc[df.channel == "EC", "unit_price"] *= 0.95と、条件を満たす行だけをlocで選んで一括更新しており、ループを 1 つも書いていない点が pandas 流です。- 地域×商品の集計は
groupby(["region", "product"]).agg(件数=("amount", "size"), ...)という「名前付き集計」で、列名・対象列・統計量を 1 行ずつ宣言的に指定しています。 pd.pivot_table(df, index="region", columns="channel", values="amount", aggfunc="sum", margins=True)は Excel のピボットに相当し、margins=Trueが総計行・列(「合計」)を付けます。- 構成比は
share.div(share.sum(axis=1), axis=0)で計算します。行ごとの合計で各セルを割る「軸を指定したブロードキャスト」の例です。 - 時系列処理は
df.set_index("date")["amount"].resample("ME").sum()で日次データを月末(ME = Month End)単位に集約し、rolling(3).mean()(移動平均)、pct_change()(前月比)、cumsum()(累積)を列として追加します。 - 商品マスタとの結合は
df.merge(master, on="product", how="left")。その後.assign(粗利率=lambda d: ...)のように、中間変数を作らず処理をつなぐメソッドチェーンで粗利率まで一気に計算しています。
実行結果の見方
[型と欠損] で date 列が datetime64 になっていることが、後の resample を可能にしています。ピボットテーブルでは東京の売上が 44,332,430 円と全体の 4 割超を占め、構成比を見るとどの地域も店舗:EC がおおむね 6:4 であることが分かります(EC の 5% 割引を反映して EC 比率はやや低め)。
月次リサンプルでは 2025-07 の前月比 +70.8% が目を引きますが、3ヶ月移動平均の列を見ると単月の振れがならされて傾向が読みやすくなることが確認できます。最後のパレート分析では、上位 20%(200 件)の取引が売上の 50.0% を占めており、「売上は少数の大口取引に偏る」という実務でよく観察される構造が再現されています。
ソースコード
# /// script
# requires-python = ">=3.11"
# dependencies = [
# "numpy",
# "pandas",
# ]
# ///
"""12: pandas によるデータ操作 -------------------------------------------
テーマ: 売上データのグループ集計・ピボット・時系列リサンプル・移動平均
学習点: DataFrame, groupby/agg, pivot_table, resample, rolling, merge,
メソッドチェーン
"""
import numpy as np
import pandas as pd
rng = np.random.default_rng(0)
pd.set_option("display.unicode.east_asian_width", True)
def make_data(n: int = 1000) -> pd.DataFrame:
dates = pd.to_datetime("2025-01-01") + pd.to_timedelta(
rng.integers(0, 365, n), unit="D")
df = pd.DataFrame({
"date": dates,
"region": rng.choice(["東京", "大阪", "名古屋", "福岡"], n,
p=[0.45, 0.25, 0.18, 0.12]),
"channel": rng.choice(["店舗", "EC"], n, p=[0.6, 0.4]),
"product": rng.choice(["A", "B", "C"], n),
"qty": rng.integers(1, 15, n),
})
price = {"A": 12000, "B": 4800, "C": 25000}
df["unit_price"] = df["product"].map(price)
# ECは若干割引
df.loc[df.channel == "EC", "unit_price"] *= 0.95
df["amount"] = df.unit_price * df.qty
return df.sort_values("date").reset_index(drop=True)
def main() -> None:
df = make_data()
print("[先頭5行]")
print(df.head(), "\n")
print("[型と欠損]")
print(df.dtypes.to_string(), "\n")
print("[地域×商品の集計(複数統計量)]")
g = df.groupby(["region", "product"]).agg(
件数=("amount", "size"),
売上合計=("amount", "sum"),
平均単価=("unit_price", "mean"),
数量中央値=("qty", "median"),
).round(0)
print(g, "\n")
print("[ピボットテーブル: 行=地域, 列=チャネル, 値=売上合計]")
pv = pd.pivot_table(df, index="region", columns="channel",
values="amount", aggfunc="sum", margins=True,
margins_name="合計")
print(pv.map(lambda v: f"{v:,.0f}"), "\n")
print("[構成比(行方向の割合)]")
share = pv.iloc[:-1, :-1]
print((share.div(share.sum(axis=1), axis=0) * 100).round(1), "\n")
print("[月次リサンプルと3ヶ月移動平均]")
ts = (df.set_index("date")["amount"]
.resample("ME").sum()
.to_frame("月次売上"))
ts["3ヶ月移動平均"] = ts["月次売上"].rolling(3).mean()
ts["前月比"] = ts["月次売上"].pct_change()
ts["累積"] = ts["月次売上"].cumsum()
print(ts.assign(**{
"月次売上": lambda d: d["月次売上"].map("{:,.0f}".format),
"3ヶ月移動平均": lambda d: d["3ヶ月移動平均"].map(
lambda v: "-" if pd.isna(v) else f"{v:,.0f}"),
"前月比": lambda d: d["前月比"].map(
lambda v: "-" if pd.isna(v) else f"{v:+.1%}"),
"累積": lambda d: d["累積"].map("{:,.0f}".format),
}), "\n")
print("[マスタとの結合 (merge) と条件付き集計]")
master = pd.DataFrame({"product": ["A", "B", "C"],
"カテゴリ": ["家電", "消耗品", "家電"],
"原価率": [0.62, 0.45, 0.70]})
m = df.merge(master, on="product", how="left")
m["粗利"] = m.amount * (1 - m.原価率)
print(m.groupby("カテゴリ")[["amount", "粗利"]].sum()
.rename(columns={"amount": "売上"})
.assign(粗利率=lambda d: (d.粗利 / d.売上).map("{:.1%}".format))
.map(lambda v: f"{v:,.0f}" if isinstance(v, (int, float)) else v))
print("\n[上位20%の顧客ならぬ上位取引が売上に占める割合(パレート)]")
s = df.amount.sort_values(ascending=False)
top = int(len(s) * 0.2)
print(f" 上位20%({top}件)の売上シェア = {s[:top].sum() / s.sum():.1%}")
if __name__ == "__main__":
main()
実行結果
[先頭5行]
date region channel product qty unit_price amount
0 2025-01-01 東京 店舗 C 8 25000 200000
1 2025-01-01 東京 店舗 A 5 12000 60000
2 2025-01-02 東京 EC B 6 4560 27360
3 2025-01-02 大阪 EC B 12 4560 54720
4 2025-01-02 東京 店舗 C 4 25000 100000
[型と欠損]
date datetime64[us]
region str
channel str
product str
qty int64
unit_price int64
amount int64
[地域×商品の集計(複数統計量)]
件数 売上合計 平均単価 数量中央値
region product
名古屋 A 71 5812800 11780.0 7.0
B 62 2059680 4715.0 7.0
C 64 11141250 24492.0 7.0
大阪 A 81 7630800 11778.0 8.0
B 80 3007200 4713.0 8.0
C 82 14956250 24527.0 8.0
東京 A 144 12302400 11788.0 7.0
B 159 5531280 4723.0 7.0
C 150 26498750 24467.0 7.0
福岡 A 40 3315000 11790.0 7.0
B 38 1189680 4693.0 6.0
C 29 5278750 24612.0 7.0
[ピボットテーブル: 行=地域, 列=チャネル, 値=売上合計]
channel EC 店舗 合計
region
名古屋 7,441,730 11,572,000 19,013,730
大阪 9,480,050 16,114,200 25,594,250
東京 16,324,230 28,008,200 44,332,430
福岡 3,088,830 6,694,600 9,783,430
合計 36,334,840 62,389,000 98,723,840
[構成比(行方向の割合)]
channel EC 店舗
region
名古屋 39.1 60.9
大阪 37.0 63.0
東京 36.8 63.2
福岡 31.6 68.4
[月次リサンプルと3ヶ月移動平均]
月次売上 3ヶ月移動平均 前月比 累積
date
2025-01-31 6,313,020 - - 6,313,020
2025-02-28 7,516,630 - +19.1% 13,829,650
2025-03-31 7,197,030 7,008,893 -4.3% 21,026,680
2025-04-30 8,601,970 7,771,877 +19.5% 29,628,650
2025-05-31 7,756,510 7,851,837 -9.8% 37,385,160
2025-06-30 6,990,160 7,782,880 -9.9% 44,375,320
2025-07-31 11,938,220 8,894,963 +70.8% 56,313,540
2025-08-31 9,736,110 9,554,830 -18.4% 66,049,650
2025-09-30 8,050,300 9,908,210 -17.3% 74,099,950
2025-10-31 9,545,900 9,110,770 +18.6% 83,645,850
2025-11-30 7,876,770 8,490,990 -17.5% 91,522,620
2025-12-31 7,201,220 8,207,963 -8.6% 98,723,840
[マスタとの結合 (merge) と条件付き集計]
売上 粗利 粗利率
カテゴリ
家電 86,936,000 28,405,680 32.7%
消耗品 11,787,840 6,483,312 55.0%
[上位20%の顧客ならぬ上位取引が売上に占める割合(パレート)]
上位20%(200件)の売上シェア = 50.0%