Skip to the content.

← 目次← 前: 11次: 13 →

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% の取引が売上に占めるシェア(パレート分析)、と実務で頻出する操作を一通りたどります。

コードの読みどころ

実行結果の見方

[型と欠損] で 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%

← 目次← 前: 11次: 13 →