100本ノック / Python / データ分析のためのPython入門100本ノック

データ分析の基本的な流れを理解する

製品別・月別の販売実績を集計・可視化して年次戦略レポートを作成する

100本ノック 第10章(No.091〜No.100):集計・可視化・ミニ分析

本記事は「データ分析のための Python 入門 100本ノック」シリーズの 最終章(第10章) です。
第9章(No.081〜090)では Polars によるデータ加工(フィルタ・列追加・欠損処理)を学びました。
本章では 集計・可視化・ミニ分析 として、group_by による集計・日付処理・matplotlib による
グラフ作成・相関分析・総合レポート出力まで、Python データ分析の一連の流れ を習得します。

[!NOTE] 本資料は、数理工房 (もしくは代表である和山個人) が過去に企業研修において使用した notebook を 企業様の許可を得て再構成・編集のうえ公開しています。
掲載データはすべて架空のものであり、実在する企業・工場・数値とは一切関係ありません。

はじめに:この記事で扱う製造業の実務課題

産業用センサーメーカーの経営企画チーム M さんの年末業務です。

年末の定例作業
  1. 営業システムから2024年の全取引データをCSVでエクスポート(約280件)
  2. 製品カテゴリ別の年間売上を集計(温度・圧力・流量・振動・電流センサー)
  3. 月次売上推移グラフを作成して取締役会向けに報告
  4. 単価と数量の相関を確認して来期の価格戦略を検討
  5. 上記すべてを1つの分析レポートとして出力

Python の Polars + matplotlib を使えば、
280件 × 9列のデータから 製品戦略に直結する洞察
数十行のコードで一括出力できます。
本章(最終章)では、ここまで学んできた Python スキルを総動員します。

現場でよくある状況

場面課題
カテゴリ集計製品カテゴリ別売上を Excel のピボットで手作業集計(毎月1時間以上)
月次推移月別売上グラフを Excel で手作業で更新(コピー&貼り付けミス多発)
分布確認取引金額のばらつきを直感だけで把握(外れ値の見落とし)
相関分析「単価が高い製品は数量が売れない気がする」を勘で判断
レポート作成集計・グラフ・コメントを別々のファイルで管理(整合性が取れない)

今回の目標: Python で集計 → 可視化 → 相関分析 → レポート出力まで
一つの notebook にまとめ、再現可能な年次分析パイプライン を構築します。

なぜこの問題は判断が難しいのか

集計・可視化・相関分析の落とし穴を整理します。

  1. group_by の集計関数の選択
    sum(合計)と mean(平均)は全く異なる意味を持ちます。
    「カテゴリ別売上合計」と「カテゴリ別平均取引単価」を混同すると、
    誤った製品戦略につながります。

  2. 日付型の変換漏れ
    文字列のまま月別集計すると「2024-1」と「2024-10」の並びが
    辞書順(文字列順)になり、時系列グラフが崩れます。
    必ず Date 型に変換してから集計・ソートする必要があります。

  3. ヒストグラムの bin 数選択
    bin 数が少なすぎると分布の形状を見落とし、
    多すぎるとノイズに引きずられます。
    データ件数の平方根(n\sqrt{n})を目安とするスタージェスの公式が参考になります。

  4. 相関と因果の混同
    単価と数量に負の相関があっても、
    「単価を下げれば数量が増える」とは言えません。
    相関はあくまで 関連性の強さ を示すものであり、因果関係の証明ではありません。

  5. スケールの違いによるグラフの誤読
    2軸グラフや単位の異なる棒グラフは、印象操作になりえます。
    軸の単位・スケールを必ず明示し、比較可能な形式で可視化しましょう。

今回扱うノックの全体像

No.タイトル製造業での活用場面
091group_byでカテゴリ別に集計する製品カテゴリ別・顧客タイプ別の年間売上集計
092商品別売上を集計する製品名別売上ランキングで主力製品の特定
093日付列をDatetime型に変換する年月の正確な時系列集計のための型変換
094月別売上を集計する月次売上推移と季節性の把握
095matplotlibで折れ線グラフを描く月次売上推移の視覚化
096棒グラフでカテゴリ別売上を可視化する製品カテゴリ別・顧客タイプ別の構成比
097ヒストグラムで数値の分布を見る取引金額・単価分布から外れ値・偏りを検出
098散布図で2つの変数の関係を見る単価 vs 数量の散布図で価格戦略を検討
099相関係数を確認する売上KPI間の相関係数行列で要因分析
100売上データを読み込み分析レポートを作成する1つのスクリプトで集計・グラフ・サマリーを一括出力

Python 環境の準備

import subprocess, sys

res = subprocess.run(["sw_vers", "-productVersion"], capture_output=True, text=True)
print(f"macOS : {res.stdout.strip()}")
print(f"Python: {sys.version}")
macOS : 26.3
Python: 3.13.1 (main, Dec  3 2024, 17:59:52) [Clang 16.0.0 (clang-1600.0.26.4)]
import numpy as np
import polars as pl
import matplotlib
import matplotlib.pyplot as plt
import csv, os, datetime

matplotlib.rcParams['font.family'] = 'Hiragino Maru Gothic Pro'
%config InlineBackend.figure_format = 'svg'
np.random.seed(42)

print("ライブラリ読み込み完了")
print(f"  NumPy     : {np.__version__}")
print(f"  Polars    : {pl.__version__}")
print(f"  Matplotlib: {matplotlib.__version__}")
ライブラリ読み込み完了
  NumPy     : 2.5.1
  Polars    : 1.42.1
  Matplotlib: 3.11.0

架空データの作成

想定シナリオ: 産業用センサーメーカー / 経営企画チーム
期間: 2024年1月〜12月(全12ヶ月)
製品: 5カテゴリ(温度・圧力・流量・振動・電流センサー)
顧客: 5タイプ(自動車・食品・半導体・医療・一般産業)
出力: 年次販売実績 CSV(sensor_sales_2024.csv

np.random.seed(42)

# 製品カテゴリ設定
PRODUCTS = {
    "温度センサー": {"prefix": "TS", "price_lo": 8_000,  "price_hi": 25_000, "base_qty": 50, "weight": 0.28},
    "圧力センサー": {"prefix": "PS", "price_lo": 12_000, "price_hi": 45_000, "base_qty": 32, "weight": 0.22},
    "流量センサー": {"prefix": "FS", "price_lo": 18_000, "price_hi": 62_000, "base_qty": 24, "weight": 0.18},
    "振動センサー": {"prefix": "VS", "price_lo": 22_000, "price_hi": 80_000, "base_qty": 18, "weight": 0.14},
    "電流センサー": {"prefix": "CS", "price_lo": 5_000,  "price_hi": 18_000, "base_qty": 60, "weight": 0.18},
}
CAT_NAMES  = list(PRODUCTS.keys())
CAT_WEIGHTS = [v["weight"] for v in PRODUCTS.values()]

CUST_TYPES   = ["自動車", "食品", "半導体", "医療", "一般産業"]
CUST_WEIGHTS = [0.32, 0.22, 0.24, 0.14, 0.08]

# 月次季節性(1.0 = 平均)
SEASONALITY = [0.85, 0.80, 0.95, 1.00, 1.05, 1.10,
               0.92, 0.88, 1.10, 1.15, 1.22, 0.98]

records = []
for month in range(1, 13):
    s = SEASONALITY[month - 1]
    n = max(18, min(38, int(np.random.normal(26, 5))))
    for _ in range(n):
        cat  = np.random.choice(CAT_NAMES, p=CAT_WEIGHTS)
        info = PRODUCTS[cat]
        model = np.random.choice(["A", "B", "C", "S"])
        product_name = f"{info['prefix']}-{model}"

        cust_type = np.random.choice(CUST_TYPES, p=CUST_WEIGHTS)
        cust_id   = f"{cust_type[:2]}-{np.random.randint(1000, 9999):04d}"

        # 単価:カテゴリ固有の範囲 × 季節変動
        unit_price = int(np.random.uniform(info['price_lo'], info['price_hi']))
        # 数量:高単価ほど少なく(逆相関を持たせる)
        price_mid  = (info['price_lo'] + info['price_hi']) / 2
        qty_factor = 1.5 - (unit_price / price_mid) * 0.5
        quantity   = max(1, int(np.random.normal(info['base_qty'] * s * qty_factor,
                                                  info['base_qty'] * 0.25)))
        sales_amount = unit_price * quantity

        # 返品フラグ(高単価ほどわずかに高い)
        ret_prob = 0.02 + (unit_price - info['price_lo']) / (info['price_hi'] - info['price_lo']) * 0.04
        is_returned = 1 if np.random.random() < ret_prob else 0

        day = np.random.randint(1, 29)
        date_str = f"2024-{month:02d}-{day:02d}"

        records.append({
            "date": date_str,
            "product_category": cat,
            "product_name": product_name,
            "customer_type": cust_type,
            "customer_id": cust_id,
            "quantity": quantity,
            "unit_price": unit_price,
            "sales_amount": sales_amount,
            "is_returned": is_returned,
        })

CSV_PATH = "sensor_sales_2024.csv"
with open(CSV_PATH, "w", newline="", encoding="utf-8") as f:
    writer = csv.DictWriter(f, fieldnames=records[0].keys())
    writer.writeheader()
    writer.writerows(records)

print(f"生成完了: {CSV_PATH}")
print(f"  レコード数  : {len(records):,} 件")
print(f"  列数        : {len(records[0])} 列")
print(f"  期間        : 2024-01-01 〜 2024-12-28")
print(f"  総売上金額  : {sum(r['sales_amount'] for r in records):,} 円")
print(f"  返品件数    : {sum(r['is_returned'] for r in records)} 件")
生成完了: sensor_sales_2024.csv
  レコード数  : 301 件
  列数        : 9 列
  期間        : 2024-01-01 〜 2024-12-28
  総売上金額  : 239,354,914 円
  返品件数    : 10 件

No.091:group_byでカテゴリ別に集計する

実務での意味

group_by は製造業の集計業務の核心です。
「製品カテゴリ別に年間売上を集計する」「顧客タイプ別の取引件数を把握する」
といった操作は、Excel のピボットテーブルに相当しますが、
Python/Polars では コード一行 で実現でき、データ量が増えても速度が落ちません。

製造業での主な活用:

  • ライン別・製品別の月次不良数集計
  • 顧客タイプ別の受注件数・売上金額の把握
  • 工程別の平均サイクルタイム集計

分析・モデル化の考え方

group_by の基本構文:

df.group_by("カテゴリ列").agg([
    pl.col("数値列").sum().alias("合計"),
    pl.col("数値列").mean().alias("平均"),
    pl.len().alias("件数"),
])

集計関数の選択が重要です。sum(合計)は全体規模の把握に、
mean(平均)は単位あたりのパフォーマンス比較に使います。
両方を同時に計算することで、規模と効率を並べて評価できます。

Pythonで確認する

# CSV を読み込む(以降のノックでも使いまわす)
df_raw = pl.read_csv(CSV_PATH, try_parse_dates=False)

# 製品カテゴリ別 集計
cat_summary = (
    df_raw.group_by("product_category")
    .agg([
        pl.col("sales_amount").sum().alias("total_sales"),
        pl.col("quantity").sum().alias("total_qty"),
        pl.col("unit_price").mean().alias("avg_unit_price"),
        pl.len().alias("transaction_count"),
    ])
    .sort("total_sales", descending=True)
)
print("製品カテゴリ別 集計:")
print(cat_summary)
print()

# 顧客タイプ別 集計
cust_summary = (
    df_raw.group_by("customer_type")
    .agg([
        pl.col("sales_amount").sum().alias("total_sales"),
        pl.len().alias("transaction_count"),
        pl.col("is_returned").mean().alias("return_rate"),
    ])
    .sort("total_sales", descending=True)
)
print("顧客タイプ別 集計:")
print(cust_summary)
製品カテゴリ別 集計:
shape: (5, 5)
┌──────────────────┬─────────────┬───────────┬────────────────┬───────────────────┐
│ product_category ┆ total_sales ┆ total_qty ┆ avg_unit_price ┆ transaction_count │
│ ---              ┆ ---         ┆ ---       ┆ ---            ┆ ---               │
│ str              ┆ i64         ┆ i64       ┆ f64            ┆ u32               │
╞══════════════════╪═════════════╪═══════════╪════════════════╪═══════════════════╡
│ 流量センサー     ┆ 64369428    ┆ 1658      ┆ 40617.757143   ┆ 70                │
│ 温度センサー     ┆ 60772674    ┆ 4166      ┆ 15267.060241   ┆ 83                │
│ 振動センサー     ┆ 41410765    ┆ 781       ┆ 55833.916667   ┆ 48                │
│ 圧力センサー     ┆ 41210887    ┆ 1610      ┆ 27255.176471   ┆ 51                │
│ 電流センサー     ┆ 31591160    ┆ 2833      ┆ 11778.877551   ┆ 49                │
└──────────────────┴─────────────┴───────────┴────────────────┴───────────────────┘

顧客タイプ別 集計:
shape: (5, 4)
┌───────────────┬─────────────┬───────────────────┬─────────────┐
│ customer_type ┆ total_sales ┆ transaction_count ┆ return_rate │
│ ---           ┆ ---         ┆ ---               ┆ ---         │
│ str           ┆ i64         ┆ u32               ┆ f64         │
╞═══════════════╪═════════════╪═══════════════════╪═════════════╡
│ 自動車        ┆ 84512570    ┆ 105               ┆ 0.028571    │
│ 半導体        ┆ 53920928    ┆ 70                ┆ 0.071429    │
│ 食品          ┆ 48413908    ┆ 60                ┆ 0.0         │
│ 医療          ┆ 36645931    ┆ 47                ┆ 0.021277    │
│ 一般産業      ┆ 15861577    ┆ 19                ┆ 0.052632    │
└───────────────┴─────────────┴───────────────────┴─────────────┘

結果の読み取り

  • 製品カテゴリ別では、total_sales(売上合計)と avg_unit_price(平均単価)を並べることで、
    「高単価だが件数が少ない製品」vs「低単価だが大量に売れている製品」の特性が見えます。
  • 顧客タイプ別では、return_rate(返品率)が顧客タイプによって異なる場合、
    特定顧客への品質改善・仕様確認が優先課題になります。
  • group_bysort と組み合わせることで、
    売上ランキング形式の集計表 を即座に作成できます。

No.092:商品別売上を集計する

実務での意味

製品カテゴリよりも細かい 製品名(モデル)別 の集計は、
「どのモデルが主力か」「廃番候補はどれか」を判断するための基礎データです。
製造業では SKU(Stock Keeping Unit)別の売上管理が在庫計画・生産計画に直結します。

分析・モデル化の考え方

製品別売上ランキングは ABC 分析(パレート分析) の出発点です。

累積売上比率=i=1ksii=1nsi×100\text{累積売上比率} = \frac{\sum_{i=1}^{k} s_i}{\sum_{i=1}^{n} s_i} \times 100

ここで sis_i は売上上位 ii 番目の製品の売上金額、nn は全製品数です。
上位 20% の製品が売上全体の 80% を占める「パレートの法則(80:20 の法則)」が
製造業では頻繁に確認されます。これを利用して重点管理製品を絞り込みます。

Pythonで確認する

# 製品名別 売上ランキング
product_ranking = (
    df_raw.group_by("product_name")
    .agg([
        pl.col("sales_amount").sum().alias("total_sales"),
        pl.col("quantity").sum().alias("total_qty"),
        pl.col("unit_price").mean().alias("avg_price"),
        pl.len().alias("count"),
    ])
    .sort("total_sales", descending=True)
)

# 累積売上比率(ABC 分析の準備)
total = product_ranking["total_sales"].sum()
cumsum = product_ranking["total_sales"].cum_sum()
product_ranking = product_ranking.with_columns(
    (cumsum / total * 100).round(1).alias("cumulative_pct")
)

print("製品名別 売上ランキング(上位10件):")
print(product_ranking.head(10))
print()

# ABC 分類
a_products = product_ranking.filter(pl.col("cumulative_pct") <= 70)
b_products = product_ranking.filter((pl.col("cumulative_pct") > 70) & (pl.col("cumulative_pct") <= 90))
c_products = product_ranking.filter(pl.col("cumulative_pct") > 90)

print(f"ABC 分析結果:")
print(f"  A ランク(累積 0〜70%) : {len(a_products)} 製品")
print(f"  B ランク(累積 70〜90%): {len(b_products)} 製品")
print(f"  C ランク(累積 90〜100%): {len(c_products)} 製品")
print(f"  全製品数: {len(product_ranking)} 製品")
製品名別 売上ランキング(上位10件):
shape: (10, 6)
┌──────────────┬─────────────┬───────────┬──────────────┬───────┬────────────────┐
│ product_name ┆ total_sales ┆ total_qty ┆ avg_price    ┆ count ┆ cumulative_pct │
│ ---          ┆ ---         ┆ ---       ┆ ---          ┆ ---   ┆ ---            │
│ str          ┆ i64         ┆ i64       ┆ f64          ┆ u32   ┆ f64            │
╞══════════════╪═════════════╪═══════════╪══════════════╪═══════╪════════════════╡
│ TS-A         ┆ 20271994    ┆ 1427      ┆ 14894.714286 ┆ 28    ┆ 8.5            │
│ FS-S         ┆ 19761553    ┆ 556       ┆ 37514.227273 ┆ 22    ┆ 16.7           │
│ FS-A         ┆ 19456558    ┆ 461       ┆ 43442.75     ┆ 20    ┆ 24.9           │
│ TS-S         ┆ 16905889    ┆ 1089      ┆ 16451.045455 ┆ 22    ┆ 31.9           │
│ FS-B         ┆ 15942962    ┆ 425       ┆ 39620.166667 ┆ 18    ┆ 38.6           │
│ VS-C         ┆ 12852436    ┆ 278       ┆ 48262.533333 ┆ 15    ┆ 43.9           │
│ PS-C         ┆ 12773479    ┆ 418       ┆ 31347.071429 ┆ 14    ┆ 49.3           │
│ TS-C         ┆ 12341172    ┆ 774       ┆ 16469.6875   ┆ 16    ┆ 54.4           │
│ VS-A         ┆ 11598360    ┆ 216       ┆ 57092.923077 ┆ 13    ┆ 59.3           │
│ TS-B         ┆ 11253619    ┆ 876       ┆ 13216.235294 ┆ 17    ┆ 64.0           │
└──────────────┴─────────────┴───────────┴──────────────┴───────┴────────────────┘

ABC 分析結果:
  A ランク(累積 0〜70%) : 11 製品
  B ランク(累積 70〜90%): 5 製品
  C ランク(累積 90〜100%): 4 製品
  全製品数: 20 製品

結果の読み取り

  • 累積売上比率cumulative_pct)を確認することで、
    売上の 70% を担う A ランク製品 を即座に特定できます。
  • A ランク製品は在庫を厚めに持ち、生産優先度を上げる判断の根拠になります。
  • C ランク製品は廃番・価格改定・製品統合の候補として経営会議に提示できます。
  • cum_sum() を使った累積比率の計算は Polars で1行で書けます。

No.093:日付列をDatetime型に変換する

実務での意味

日付型の変換は 時系列分析の前提条件 です。
文字列のままでは「2024-9 < 2024-10」という辞書順比較になり、
グラフの時系列軸が崩れます。
製造業では「月次生産数推移」「週次不良率推移」「四半期別売上」など、
時系列での集計・可視化が非常に多いため、日付型の扱いは必須スキルです。

分析・モデル化の考え方

Polars での日付型変換の流れ:

ステップ操作Polars コード
① 文字列 → Date 型str.to_date で変換pl.col("date").str.to_date("%Y-%m-%d")
② 年を抽出dt.yearpl.col("date").dt.year()
③ 月を抽出dt.monthpl.col("date").dt.month()
④ 年月文字列を生成dt.to_stringpl.col("date").dt.to_string("%Y-%m")
⑤ 日付でソートsort("date")文字列ではなく日付順で並ぶ

Pythonで確認する

# 日付列を Date 型に変換
df = df_raw.with_columns(
    pl.col("date").str.to_date("%Y-%m-%d")
)

# 年・月・年月列を追加
df = df.with_columns([
    pl.col("date").dt.year().alias("year"),
    pl.col("date").dt.month().alias("month"),
    pl.col("date").dt.to_string("%Y-%m").alias("year_month"),
])

print("日付型変換後の schema:")
for col, dtype in df.schema.items():
    print(f"  {col:22s}: {dtype}")
print()

print("先頭3行(date列・year・month・year_month の確認):")
print(df.select(["date", "year", "month", "year_month"]).head(3))
print()

# 型変換前後の比較
print("型変換の確認:")
print(f"  変換前: {df_raw['date'].dtype}  例: {df_raw['date'][0]}")
print(f"  変換後: {df['date'].dtype}    例: {df['date'][0]}")
print()

# 月の一覧確認(ソートが文字列ではなく日付順になる)
months_sorted = df["year_month"].unique().sort()
print("年月の一覧(ソート確認):")
print(months_sorted.to_list())
日付型変換後の schema:
  date                  : Date
  product_category      : String
  product_name          : String
  customer_type         : String
  customer_id           : String
  quantity              : Int64
  unit_price            : Int64
  sales_amount          : Int64
  is_returned           : Int64
  year                  : Int32
  month                 : Int8
  year_month            : String

先頭3行(date列・year・month・year_month の確認):
shape: (3, 4)
┌────────────┬──────┬───────┬────────────┐
│ date       ┆ year ┆ month ┆ year_month │
│ ---        ┆ ---  ┆ ---   ┆ ---        │
│ date       ┆ i32  ┆ i8    ┆ str        │
╞════════════╪══════╪═══════╪════════════╡
│ 2024-01-24 ┆ 2024 ┆ 1     ┆ 2024-01    │
│ 2024-01-28 ┆ 2024 ┆ 1     ┆ 2024-01    │
│ 2024-01-27 ┆ 2024 ┆ 1     ┆ 2024-01    │
└────────────┴──────┴───────┴────────────┘

型変換の確認:
  変換前: String  例: 2024-01-24
  変換後: Date    例: 2024-01-24

年月の一覧(ソート確認):
['2024-01', '2024-02', '2024-03', '2024-04', '2024-05', '2024-06', '2024-07', '2024-08', '2024-09', '2024-10', '2024-11', '2024-12']

結果の読み取り

  • str.to_date("%Y-%m-%d") で文字列が Date 型に変換され、
    dt.year(), dt.month(), dt.to_string() で時系列情報を取り出せます。
  • 年月の一覧が 2024-012024-12正しい時系列順に並ぶことが確認できます。
    文字列のままだと 2024-12024-102024-112024-122024-2
    という辞書順になってしまいます。
  • 以降のノックでは、この df(日付型変換済み)を使用します。

No.094:月別売上を集計する

実務での意味

月次売上推移は経営会議・取締役会で最も頻繁に報告される KPI です。
「今月は先月比 +5%」「Q4 は前年同期比 +12%」といった分析の出発点が
月別集計です。Polars の group_by + sort で月次集計表を即座に作成できます。

分析・モデル化の考え方

月次集計に加えて、前月比成長率を計算することで
トレンドの加速・減速を把握できます。

前月比成長率:

MoM成長率(t)=StSt1St1×100[%]\text{MoM成長率}(t) = \frac{S_t - S_{t-1}}{S_{t-1}} \times 100 \, [\%]

ここで StS_t は月 tt の売上金額です。
製造業では季節性(繁忙期・閑散期)の影響があるため、
前月比だけでなく 前年同月比(YoY) も重要です。
本章では前月比を中心に扱います。

Pythonで確認する

# 月別売上集計
monthly = (
    df.group_by("year_month")
    .agg([
        pl.col("sales_amount").sum().alias("monthly_sales"),
        pl.col("quantity").sum().alias("monthly_qty"),
        pl.col("unit_price").mean().alias("avg_unit_price"),
        pl.len().alias("transaction_count"),
        pl.col("is_returned").sum().alias("return_count"),
    ])
    .sort("year_month")
)

# 前月比成長率を追加
sales_list = monthly["monthly_sales"].to_list()
mom_list = [None] + [
    round((sales_list[i] - sales_list[i-1]) / sales_list[i-1] * 100, 1)
    for i in range(1, len(sales_list))
]
monthly = monthly.with_columns(
    pl.Series("mom_growth_pct", mom_list, dtype=pl.Float64)
)

print("月別売上集計(前月比成長率付き):")
print(monthly)
print()

# 四半期別集計
q_map = {"Q1": [1,2,3], "Q2": [4,5,6], "Q3": [7,8,9], "Q4": [10,11,12]}
print("四半期別 売上合計:")
for q, months_in_q in q_map.items():
    q_sales = df.filter(pl.col("month").is_in(months_in_q))["sales_amount"].sum()
    print(f"  {q}: {q_sales:>12,} 円")
print(f"  年間: {df['sales_amount'].sum():>12,} 円")
月別売上集計(前月比成長率付き):
shape: (12, 7)
┌────────────┬──────────────┬─────────────┬──────────────┬─────────────┬─────────────┬─────────────┐
│ year_month ┆ monthly_sale ┆ monthly_qty ┆ avg_unit_pri ┆ transaction ┆ return_coun ┆ mom_growth_ │
│ ---        ┆ s            ┆ ---         ┆ ce           ┆ _count      ┆ t           ┆ pct         │
│ str        ┆ ---          ┆ i64         ┆ ---          ┆ ---         ┆ ---         ┆ ---         │
│            ┆ i64          ┆             ┆ f64          ┆ u32         ┆ i64         ┆ f64         │
╞════════════╪══════════════╪═════════════╪══════════════╪═════════════╪═════════════╪═════════════╡
│ 2024-01    ┆ 19526168     ┆ 862         ┆ 27905.642857 ┆ 28          ┆ 3           ┆ null        │
│ 2024-02    ┆ 17564379     ┆ 795         ┆ 28723.214286 ┆ 28          ┆ 1           ┆ -10.0       │
│ 2024-03    ┆ 22823593     ┆ 917         ┆ 33778.551724 ┆ 29          ┆ 0           ┆ 29.9        │
│ 2024-04    ┆ 15582567     ┆ 729         ┆ 28245.157895 ┆ 19          ┆ 0           ┆ -31.7       │
│ 2024-05    ┆ 22778096     ┆ 987         ┆ 30078.125    ┆ 24          ┆ 1           ┆ 46.2        │
│ …          ┆ …            ┆ …           ┆ …            ┆ …           ┆ …           ┆ …           │
│ 2024-08    ┆ 20222658     ┆ 774         ┆ 33197.814815 ┆ 27          ┆ 2           ┆ 13.8        │
│ 2024-09    ┆ 25273346     ┆ 1271        ┆ 25750.896552 ┆ 29          ┆ 1           ┆ 25.0        │
│ 2024-10    ┆ 21004719     ┆ 1095        ┆ 26097.48     ┆ 25          ┆ 0           ┆ -16.9       │
│ 2024-11    ┆ 22322934     ┆ 1083        ┆ 28648.75     ┆ 24          ┆ 0           ┆ 6.3         │
│ 2024-12    ┆ 16845974     ┆ 770         ┆ 28388.434783 ┆ 23          ┆ 0           ┆ -24.5       │
└────────────┴──────────────┴─────────────┴──────────────┴─────────────┴─────────────┴─────────────┘

四半期別 売上合計:
  Q1:   59,914,140 円
  Q2:   56,000,968 円
  Q3:   63,266,179 円
  Q4:   60,173,627 円
  年間:  239,354,914 円

結果の読み取り

  • 月次集計表から売上の季節性が読み取れます。
    成長率がプラスの月は伸長期、マイナスは減速期として戦略的に解釈します。
  • 四半期別集計で Q4(10〜12月)の売上構成比が高い場合、
    年末の集中受注リスク(生産・在庫の逼迫)を事前に計画できます。
  • mom_growth_pct(前月比成長率)がマイナスに転じた月は、
    営業チームへのヒアリング・競合動向調査のトリガーとして活用できます。

No.095:matplotlibで折れ線グラフを描く

実務での意味

折れ線グラフは 時系列データの変化傾向を視覚化 する最適な手段です。
月次売上推移を折れ線グラフで表現することで、
「どの月に急増・急落したか」「Q4 に向けて右肩上がりか」が
一目で経営層に伝わります。数字の表よりもグラフの方が
意思決定のスピード を大幅に上げます。

分析・モデル化の考え方

時系列グラフを作成する際の重要な設定項目:

設定意味コード
marker="o"各データ点を丸印で表示plt.plot(..., marker="o")
grid=Trueグリッド線で読み取りやすくax.grid(alpha=0.3)
xlabel/ylabel軸ラベル(単位を明示)ax.set_xlabel("月")
titleグラフのタイトルax.set_title(...)
tight_layout余白を自動調整plt.tight_layout()

製造業のレポートでは 上下の変動を強調したい場合は y 軸の下限を調整 し、
過度に変動を誇張しないよう注意が必要です。

Pythonで確認する

months_str = monthly["year_month"].to_list()      # ["2024-01", ..., "2024-12"]
sales_vals = monthly["monthly_sales"].to_list()
qty_vals   = monthly["monthly_qty"].to_list()
x_idx      = list(range(len(months_str)))

fig, axes = plt.subplots(2, 1, figsize=(11, 8), sharex=True)

# ── 上段: 月次売上金額の推移 ──
ax1 = axes[0]
ax1.plot(x_idx, sales_vals, marker="o", linewidth=2, color="#4878CF",
         markersize=6, label="月次売上金額")
ax1.fill_between(x_idx, sales_vals, alpha=0.12, color="#4878CF")

# 最大・最小月を強調
max_i = sales_vals.index(max(sales_vals))
min_i = sales_vals.index(min(sales_vals))
ax1.scatter([max_i], [sales_vals[max_i]], color="#D65F5F", s=100, zorder=5, label=f"最大: {months_str[max_i]}")
ax1.scatter([min_i], [sales_vals[min_i]], color="#6ACC65", s=100, zorder=5, label=f"最小: {months_str[min_i]}")

ax1.set_title("2024年 月次売上推移", fontsize=13, pad=12)
ax1.set_ylabel("売上金額(円)", fontsize=10)
ax1.yaxis.set_major_formatter(plt.FuncFormatter(lambda x, _: f"{x/1e6:.1f}M"))
ax1.grid(axis="y", alpha=0.3)
ax1.legend(fontsize=9)

# ── 下段: 月次販売数量の推移 ──
ax2 = axes[1]
ax2.plot(x_idx, qty_vals, marker="s", linewidth=2, color="#6ACC65",
         markersize=6, linestyle="--", label="月次販売数量")
ax2.set_title("2024年 月次販売数量推移", fontsize=13, pad=12)
ax2.set_xlabel("月", fontsize=10)
ax2.set_ylabel("販売数量(個)", fontsize=10)
ax2.set_xticks(x_idx)
ax2.set_xticklabels([m[5:] for m in months_str], fontsize=9)  # "01"〜"12"
ax2.grid(axis="y", alpha=0.3)
ax2.legend(fontsize=9)

plt.tight_layout()
plt.savefig("no095_monthly_trend.svg", format="svg", bbox_inches="tight")
plt.show()
print("保存完了: no095_monthly_trend.svg")

svg

保存完了: no095_monthly_trend.svg

結果の読み取り

  • 上段グラフ(売上金額) から、年間の売上推移のピーク月と底月が一目で分かります。
    マーカーで強調した最大月・最小月は、経営報告で最も説明を求められる箇所です。
  • 下段グラフ(販売数量) との比較で、「数量は増えているのに売上が伸びていない月」
    は単価が下落している可能性があり、値引き傾向や製品ミックスの変化を示唆します。
  • fill_between で面積を塗ることで、変化の大きさを視覚的に強調できます。
  • sharex=True で x 軸を共有し、2つのグラフを縦に並べると比較が容易になります。

No.096:棒グラフでカテゴリ別売上を可視化する

実務での意味

棒グラフは カテゴリ間の大小比較 に最適です。
製品カテゴリ別・顧客タイプ別の売上を棒グラフで可視化することで、
「どの製品に経営資源を集中すべきか」「どの顧客セグメントを優先すべきか」
の意思決定を数字とグラフで支援します。

分析・モデル化の考え方

複数カテゴリの比較には 積み上げ棒グラフ も有効です。
顧客タイプ別売上を製品カテゴリで積み上げると、
「どの製品が自動車向けに多く売れているか」といった 2次元の内訳 が見えます。

また、棒グラフに 数値ラベル を追加すると、
グラフと数字を同時に伝えられます:

for bar in bars:
    ax.text(bar.get_x() + bar.get_width()/2, bar.get_height(),
            f"{bar.get_height():,.0f}", ha='center', va='bottom')

Pythonで確認する

# カテゴリ別集計
cat_sales = (
    df.group_by("product_category")
    .agg(pl.col("sales_amount").sum().alias("total_sales"))
    .sort("total_sales", descending=True)
)
cust_sales = (
    df.group_by("customer_type")
    .agg(pl.col("sales_amount").sum().alias("total_sales"))
    .sort("total_sales", descending=True)
)

fig, axes = plt.subplots(1, 2, figsize=(13, 6))
COLORS = ["#4878CF", "#6ACC65", "#D65F5F", "#B47CC7", "#C4AD66"]

# ── 左: 製品カテゴリ別売上 ──
ax1 = axes[0]
cats  = cat_sales["product_category"].to_list()
csales = cat_sales["total_sales"].to_list()
bars1 = ax1.bar(cats, csales, color=COLORS[:len(cats)], alpha=0.85, edgecolor="black", linewidth=0.4)
for bar in bars1:
    ax1.text(bar.get_x() + bar.get_width() / 2,
             bar.get_height() + max(csales) * 0.01,
             f"{bar.get_height()/1e6:.1f}M", ha="center", va="bottom", fontsize=8)
ax1.set_title("製品カテゴリ別 年間売上", fontsize=12, pad=12)
ax1.set_xlabel("製品カテゴリ", fontsize=10)
ax1.set_ylabel("売上金額(円)", fontsize=10)
ax1.yaxis.set_major_formatter(plt.FuncFormatter(lambda x, _: f"{x/1e6:.0f}M"))
ax1.grid(axis="y", alpha=0.3)
ax1.tick_params(axis="x", labelsize=8)

# ── 右: 顧客タイプ別売上 ──
ax2 = axes[1]
custs   = cust_sales["customer_type"].to_list()
cssales = cust_sales["total_sales"].to_list()
bars2 = ax2.bar(custs, cssales, color=COLORS[:len(custs)], alpha=0.85, edgecolor="black", linewidth=0.4)
for bar in bars2:
    ax2.text(bar.get_x() + bar.get_width() / 2,
             bar.get_height() + max(cssales) * 0.01,
             f"{bar.get_height()/1e6:.1f}M", ha="center", va="bottom", fontsize=8)
ax2.set_title("顧客タイプ別 年間売上", fontsize=12, pad=12)
ax2.set_xlabel("顧客タイプ", fontsize=10)
ax2.set_ylabel("売上金額(円)", fontsize=10)
ax2.yaxis.set_major_formatter(plt.FuncFormatter(lambda x, _: f"{x/1e6:.0f}M"))
ax2.grid(axis="y", alpha=0.3)
ax2.tick_params(axis="x", labelsize=8)

plt.tight_layout()
plt.savefig("no096_category_bar.svg", format="svg", bbox_inches="tight")
plt.show()
print("保存完了: no096_category_bar.svg")

svg

保存完了: no096_category_bar.svg

結果の読み取り

  • 製品カテゴリ別グラフから、どのカテゴリが売上を牽引しているかが一目で分かります。
    売上上位製品への生産・在庫集中投資の根拠となります。
  • 顧客タイプ別グラフから、どの顧客セグメントが最大の収益源かが分かります。
    最大セグメントの需要変動は業績に直結するため、リスクとして経営層に報告すべきです。
  • 棒グラフの上に M(百万円) 単位の数値ラベルを付けることで、
    プレゼンの場でグラフと数字を同時に伝えられます。

No.097:ヒストグラムで数値の分布を見る

実務での意味

ヒストグラムは 数値データの分布(ばらつきと偏り)を把握 するためのグラフです。
製造業での活用:

  • 取引金額の分布 → 「小口取引が多いのか、大口取引中心か」
  • 製品単価の分布 → 「価格帯の集中ゾーンはどこか」
  • 不良数の分布 → 「通常時と異常時の境界値はどこか」

ヒストグラムで 外れ値(異常値) を発見することは、
不正取引の検出・生産異常の早期警告にも応用できます。

分析・モデル化の考え方

ヒストグラムの bin 数(区間数)の選択基準:

方法特徴
スタージェスの公式k=1+log2nk = 1 + \log_2 nシンプル・小規模データ向け
スコットの公式h=3.49σ^n1/3h = 3.49 \hat{\sigma} n^{-1/3}データのばらつきを考慮
フリードマン-ダイアコニスh=2IQRn1/3h = 2 \cdot \text{IQR} \cdot n^{-1/3}外れ値に頑健

実務では bins="auto" で matplotlib が自動選択するか、
n\sqrt{n} を目安に手動で調整します。

Pythonで確認する

sales_arr    = df["sales_amount"].to_numpy()
unit_pr_arr  = df["unit_price"].to_numpy()
n = len(sales_arr)

# スタージェスの公式による推奨 bin 数
sturges_bins = int(1 + np.log2(n))
print(f"データ件数 n = {n}")
print(f"スタージェスの公式による推奨 bin 数: k = 1 + log2({n}) = {sturges_bins}")
print()

fig, axes = plt.subplots(1, 2, figsize=(13, 5))

# ── 左: 取引単位売上金額の分布 ──
ax1 = axes[0]
ax1.hist(sales_arr, bins=sturges_bins, color="#4878CF", edgecolor="white",
         alpha=0.85, linewidth=0.5)
ax1.axvline(np.mean(sales_arr), color="#D65F5F", linestyle="--", linewidth=1.5,
            label=f"平均: {np.mean(sales_arr):,.0f} 円")
ax1.axvline(np.median(sales_arr), color="#6ACC65", linestyle="-.", linewidth=1.5,
            label=f"中央値: {np.median(sales_arr):,.0f} 円")
ax1.set_title("取引単位売上金額の分布", fontsize=12, pad=12)
ax1.set_xlabel("売上金額(円)", fontsize=10)
ax1.set_ylabel("件数", fontsize=10)
ax1.xaxis.set_major_formatter(plt.FuncFormatter(lambda x, _: f"{x/1e6:.1f}M"))
ax1.grid(axis="y", alpha=0.3)
ax1.legend(fontsize=9)

# ── 右: 製品単価の分布 ──
ax2 = axes[1]
ax2.hist(unit_pr_arr, bins=sturges_bins, color="#6ACC65", edgecolor="white",
         alpha=0.85, linewidth=0.5)
ax2.axvline(np.mean(unit_pr_arr), color="#D65F5F", linestyle="--", linewidth=1.5,
            label=f"平均: {np.mean(unit_pr_arr):,.0f} 円")
ax2.axvline(np.median(unit_pr_arr), color="#4878CF", linestyle="-.", linewidth=1.5,
            label=f"中央値: {np.median(unit_pr_arr):,.0f} 円")
ax2.set_title("製品単価の分布", fontsize=12, pad=12)
ax2.set_xlabel("単価(円)", fontsize=10)
ax2.set_ylabel("件数", fontsize=10)
ax2.xaxis.set_major_formatter(plt.FuncFormatter(lambda x, _: f"{x/1e3:.0f}K"))
ax2.grid(axis="y", alpha=0.3)
ax2.legend(fontsize=9)

plt.tight_layout()
plt.savefig("no097_histogram.svg", format="svg", bbox_inches="tight")
plt.show()
print("保存完了: no097_histogram.svg")
print()
# 分布の要約統計
print("売上金額の分布統計:")
for label, val in [
    ("平均",   np.mean(sales_arr)),
    ("中央値", np.median(sales_arr)),
    ("標準偏差", np.std(sales_arr)),
    ("最小値", np.min(sales_arr)),
    ("最大値", np.max(sales_arr)),
]:
    print(f"  {label:8s}: {val:>12,.0f} 円")
データ件数 n = 301
スタージェスの公式による推奨 bin 数: k = 1 + log2(301) = 9



svg

保存完了: no097_histogram.svg

売上金額の分布統計:
  平均      :      795,199 円
  中央値     :      747,180 円
  標準偏差    :      298,746 円
  最小値     :      205,403 円
  最大値     :    1,957,462 円

結果の読み取り

  • 売上金額のヒストグラムで分布の形状が確認できます。
    • 右裾が長い(右歪み)分布は、大口取引が少数存在することを意味します。
    • 平均 > 中央値 の場合は右歪みで、平均値が大口取引によって引き上げられています。
      このとき 中央値が「典型的な取引金額」をより正確に表します
  • 単価の分布が多峰性(複数の山)を持つ場合、
    異なる価格帯の製品が混在しており、製品セグメントが自然に分かれています。
  • スタージェスの公式 (k=1+log2nk = 1 + \log_2 n) で計算した bin 数は
    データ規模に対して適切な分解能を提供します。

No.098:散布図で2つの変数の関係を見る

実務での意味

散布図は 2変数間の関係(相関・クラスタ・外れ値) を視覚的に把握するためのグラフです。
製造業での活用:

  • 単価 vs 販売数量 → 価格弾力性の把握(価格戦略立案)
  • 生産数 vs 不良数 → 不良率の一定性の確認
  • 稼働時間 vs 生産数 → ラインの生産効率の評価

製品カテゴリで色分けすることで、
セグメント別の傾向の違い を1枚のグラフで表現できます。

分析・モデル化の考え方

散布図で見るべき3つのパターン:

  1. 正の相関: 右上がりのパターン(xx が増えると yy も増える)
  2. 負の相関: 右下がりのパターン(xx が増えると yy は減る)
  3. 無相関: 無秩序に分布(xxyy に関係なし)

単価 vs 数量は製品によって異なる傾向があります。
高単価製品は少量の大口取引が中心、低単価製品は多量の小口取引が中心という
製品タイプによるクラスタリング が散布図で確認できます。

Pythonで確認する

# カテゴリ別に散布図を作成
cat_colors = dict(zip(CAT_NAMES, ["#4878CF", "#6ACC65", "#D65F5F", "#B47CC7", "#C4AD66"]))

fig, axes = plt.subplots(1, 2, figsize=(13, 6))

# ── 左: 単価 vs 販売数量(製品カテゴリ別) ──
ax1 = axes[0]
for cat in CAT_NAMES:
    sub = df.filter(pl.col("product_category") == cat)
    ax1.scatter(
        sub["unit_price"].to_numpy(),
        sub["quantity"].to_numpy(),
        label=cat, color=cat_colors[cat],
        alpha=0.55, s=35, edgecolors="none"
    )
# 回帰直線(全データ)
up_all = df["unit_price"].to_numpy()
qty_all = df["quantity"].to_numpy()
coef = np.polyfit(up_all, qty_all, 1)
x_line = np.linspace(up_all.min(), up_all.max(), 200)
ax1.plot(x_line, np.polyval(coef, x_line), color="black", linewidth=1.2,
         linestyle="--", label=f"回帰直線 (傾き={coef[0]:.4f})")
ax1.set_title("単価 vs 販売数量(製品カテゴリ別)", fontsize=12, pad=12)
ax1.set_xlabel("単価(円)", fontsize=10)
ax1.set_ylabel("販売数量(個)", fontsize=10)
ax1.xaxis.set_major_formatter(plt.FuncFormatter(lambda x, _: f"{x/1e3:.0f}K"))
ax1.legend(fontsize=8, loc="upper right")
ax1.grid(alpha=0.25)

# ── 右: 売上金額 vs 数量 ──
ax2 = axes[1]
for cat in CAT_NAMES:
    sub = df.filter(pl.col("product_category") == cat)
    ax2.scatter(
        sub["sales_amount"].to_numpy(),
        sub["quantity"].to_numpy(),
        label=cat, color=cat_colors[cat],
        alpha=0.55, s=35, edgecolors="none"
    )
ax2.set_title("売上金額 vs 販売数量(製品カテゴリ別)", fontsize=12, pad=12)
ax2.set_xlabel("売上金額(円)", fontsize=10)
ax2.set_ylabel("販売数量(個)", fontsize=10)
ax2.xaxis.set_major_formatter(plt.FuncFormatter(lambda x, _: f"{x/1e6:.1f}M"))
ax2.legend(fontsize=8, loc="upper right")
ax2.grid(alpha=0.25)

plt.tight_layout()
plt.savefig("no098_scatter.svg", format="svg", bbox_inches="tight")
plt.show()
print(f"保存完了: no098_scatter.svg")
print(f"回帰直線の傾き(単価→数量): {coef[0]:.4f} 個/円")
print(f"  → 単価が 1,000円 上がると数量は {coef[0]*1000:.2f} 個 変化する")

svg

保存完了: no098_scatter.svg
回帰直線の傾き(単価→数量): -0.0008 個/円
  → 単価が 1,000円 上がると数量は -0.76 個 変化する

結果の読み取り

  • 単価 vs 数量の散布図で製品カテゴリごとのクラスタが確認できます。
    • 低単価カテゴリ(電流センサー): 左上(低単価・多量)に分布
    • 高単価カテゴリ(振動センサー): 右下(高単価・少量)に分布
  • 回帰直線の傾きが負 であれば、価格弾力性が存在することを示します。
    傾きが急なほど、価格変動が数量に大きく影響します(価格弾力性が高い)。
  • 回帰直線は 全カテゴリ混合 の傾向であり、カテゴリ内での傾向は異なる場合があります。
    価格戦略はカテゴリごとに分析することが重要です。

No.099:相関係数を確認する

実務での意味

相関係数は 2変数間の線形関係の強さと方向1-1 から +1+1 の数値で表します。
製造業での活用:

  • 単価と数量の相関 → 価格弾力性の定量化
  • 売上金額と返品フラグの相関 → 高売上取引の返品リスク評価
  • 月次売上と季節ダミーの相関 → 季節性の定量化

相関係数行列を ヒートマップ で可視化すると、
複数変数間の関係を一覧できます(seaborn の heatmap に相当する表現を matplotlib で実装)。

分析・モデル化の考え方

ピアソンの積率相関係数:

rXY=i=1n(xixˉ)(yiyˉ)i=1n(xixˉ)2i=1n(yiyˉ)2r_{XY} = \frac{\sum_{i=1}^n (x_i - \bar{x})(y_i - \bar{y})}{\sqrt{\sum_{i=1}^n (x_i - \bar{x})^2} \cdot \sqrt{\sum_{i=1}^n (y_i - \bar{y})^2}}

rr の解釈基準(一般的目安):

| r|r| の範囲 | 関係の強さ | |------------|----------| | 0.7r1.00.7 \le |r| \le 1.0 | 強い相関 | | 0.4r<0.70.4 \le |r| < 0.7 | 中程度の相関 | | 0.2r<0.40.2 \le |r| < 0.4 | 弱い相関 | | r<0.2|r| < 0.2 | ほぼ無相関 |

注意: 相関係数は外れ値の影響を受けやすく、
非線形の関係は検出できません。散布図と併用するのが原則です。

Pythonで確認する

# 数値列の相関係数行列
num_cols = ["quantity", "unit_price", "sales_amount", "is_returned"]
col_labels = ["数量", "単価", "売上金額", "返品"]

data_mat = np.column_stack([df[c].to_numpy() for c in num_cols])
corr_mat = np.corrcoef(data_mat, rowvar=False)

print("相関係数行列:")
header = f"{'':>8s}" + "".join(f"{lb:>8s}" for lb in col_labels)
print(header)
for i, lbl in enumerate(col_labels):
    row = f"{lbl:>8s}" + "".join(f"{corr_mat[i,j]:>8.3f}" for j in range(len(col_labels)))
    print(row)
print()

# ── 相関係数ヒートマップ(matplotlib 実装) ──
fig, ax = plt.subplots(figsize=(7, 6))
n_vars = len(col_labels)
im = ax.imshow(corr_mat, cmap="RdBu_r", vmin=-1, vmax=1, aspect="auto")
plt.colorbar(im, ax=ax, shrink=0.8, label="相関係数")

# セルに数値を表示
for i in range(n_vars):
    for j in range(n_vars):
        color = "white" if abs(corr_mat[i, j]) > 0.6 else "black"
        ax.text(j, i, f"{corr_mat[i, j]:.3f}",
                ha="center", va="center", fontsize=11, color=color, fontweight="bold")

ax.set_xticks(range(n_vars))
ax.set_yticks(range(n_vars))
ax.set_xticklabels(col_labels, fontsize=11)
ax.set_yticklabels(col_labels, fontsize=11)
ax.set_title("売上 KPI 相関係数ヒートマップ", fontsize=13, pad=12)
ax.grid(False)
plt.tight_layout()
plt.savefig("no099_correlation_heatmap.svg", format="svg", bbox_inches="tight")
plt.show()
print("保存完了: no099_correlation_heatmap.svg")
print()

# 注目すべき相関ペア
pairs = [(i, j) for i in range(n_vars) for j in range(i+1, n_vars)]
print("注目ペアの相関係数(強い順):")
sorted_pairs = sorted(pairs, key=lambda p: abs(corr_mat[p[0], p[1]]), reverse=True)
for (i, j) in sorted_pairs:
    r = corr_mat[i, j]
    strength = "強" if abs(r) >= 0.7 else "中" if abs(r) >= 0.4 else "弱"
    print(f"  {col_labels[i]:>5s} × {col_labels[j]:>5s}: r = {r:+.3f}  [{strength}]")
findfont: Failed to find font weight bold, now using 400.


相関係数行列:
              数量      単価    売上金額      返品
      数量   1.000  -0.735  -0.011  -0.002
      単価  -0.735   1.000   0.485  -0.030
    売上金額  -0.011   0.485   1.000  -0.061
      返品  -0.002  -0.030  -0.061   1.000



svg

保存完了: no099_correlation_heatmap.svg

注目ペアの相関係数(強い順):
     数量 ×    単価: r = -0.735  [強]
     単価 ×  売上金額: r = +0.485  [中]
   売上金額 ×    返品: r = -0.061  [弱]
     単価 ×    返品: r = -0.030  [弱]
     数量 ×  売上金額: r = -0.011  [弱]
     数量 ×    返品: r = -0.002  [弱]

結果の読み取り

  • 単価 × 数量の相関が負の場合(r<0r < 0)、
    「高単価製品ほど1回あたりの注文数量が少ない」という傾向が定量的に確認されます。
  • 単価 × 売上金額の相関が正の場合、
    単価の上昇が売上金額の増加に寄与していることを意味します。
  • 返品 × 各変数の相関が弱ければ、返品は特定の価格帯や数量に偏っていません。
    返品の原因は別の要因(品質問題・仕様ミス・顧客属性)にある可能性が高くなります。
  • ヒートマップは plt.imshow + Colorbar で seaborn を使わずに実装できます。
    セルへの数値テキスト追加が定量的な読み取りをサポートします。

No.100:売上データを読み込み分析レポートを作成する

実務での意味

No.100 はシリーズの最終ノックです。
ここまで学んできた「CSV 読み込み → 型変換 → 集計 → 可視化 → 相関分析」を
1つのスクリプトに統合し、再現可能な年次分析レポート を作成します。

製造業の DX において「分析の再現性」は非常に重要です。
担当者が変わっても、翌年同じ手順で分析を実行できる
Python notebook = 生きたドキュメント が目標です。

分析・モデル化の考え方

1本の分析パイプラインの構成要素:

① データ読み込み    : pl.read_csv()
② 前処理           : 型変換・列追加・欠損チェック
③ 集計             : group_by() × 複数軸
④ 可視化           : 2×2 ダッシュボード
⑤ テキストレポート  : print() で KPI サマリー

この構造は「ETL(Extract→Transform→Load) + 分析レポート」の最小単位であり、
実務の BI ダッシュボードや自動レポートシステムの土台になります。

Pythonで確認する

# ════════════════════════════════════════════════════════════
# No.100 ミニ分析レポート:産業用センサー 2024年度 年次販売分析
# ════════════════════════════════════════════════════════════

# ── Step 1: データ読み込み & 前処理 ──
df100 = pl.read_csv(CSV_PATH, try_parse_dates=False)
df100 = df100.with_columns(
    pl.col("date").str.to_date("%Y-%m-%d")
).with_columns([
    pl.col("date").dt.month().alias("month"),
    pl.col("date").dt.to_string("%Y-%m").alias("year_month"),
])

n_rows, n_cols = df100.shape
total_sales    = df100["sales_amount"].sum()
total_qty      = df100["quantity"].sum()
return_rate    = df100["is_returned"].mean() * 100

# ── Step 2: 集計 ──
# 月次
monthly100 = (
    df100.group_by("year_month")
    .agg(pl.col("sales_amount").sum().alias("monthly_sales"))
    .sort("year_month")
)
# カテゴリ別
cat100 = (
    df100.group_by("product_category")
    .agg(pl.col("sales_amount").sum().alias("cat_sales"))
    .sort("cat_sales", descending=True)
)
# 顧客タイプ別
cust100 = (
    df100.group_by("customer_type")
    .agg(pl.col("sales_amount").sum().alias("cust_sales"))
    .sort("cust_sales", descending=True)
)
# TOP3 製品
top3_products = (
    df100.group_by("product_name")
    .agg(pl.col("sales_amount").sum().alias("prod_sales"))
    .sort("prod_sales", descending=True)
    .head(3)
)

# ── Step 3: 2×2 ダッシュボード ──
fig, axes = plt.subplots(2, 2, figsize=(14, 10))
fig.suptitle("産業用センサー 2024年度 年次販売ダッシュボード", fontsize=15, fontweight="bold", y=1.01)
COLORS = ["#4878CF", "#6ACC65", "#D65F5F", "#B47CC7", "#C4AD66"]

# [0,0] 月次売上推移
ax00 = axes[0, 0]
m_idx = list(range(12))
m_sales = monthly100["monthly_sales"].to_list()
m_labels = [m[5:] for m in monthly100["year_month"].to_list()]
ax00.plot(m_idx, m_sales, marker="o", linewidth=2, color="#4878CF", markersize=5)
ax00.fill_between(m_idx, m_sales, alpha=0.1, color="#4878CF")
ax00.set_title("① 月次売上推移", fontsize=11, pad=8)
ax00.set_xlabel("月", fontsize=9)
ax00.set_ylabel("売上金額(円)", fontsize=9)
ax00.set_xticks(m_idx)
ax00.set_xticklabels(m_labels, fontsize=8)
ax00.yaxis.set_major_formatter(plt.FuncFormatter(lambda x, _: f"{x/1e6:.0f}M"))
ax00.grid(axis="y", alpha=0.3)

# [0,1] 製品カテゴリ別売上 棒グラフ
ax01 = axes[0, 1]
c_cats  = cat100["product_category"].to_list()
c_sales = cat100["cat_sales"].to_list()
ax01.bar(c_cats, c_sales, color=COLORS[:len(c_cats)], alpha=0.85, edgecolor="black", linewidth=0.4)
ax01.set_title("② 製品カテゴリ別売上", fontsize=11, pad=8)
ax01.set_xlabel("製品カテゴリ", fontsize=9)
ax01.set_ylabel("売上金額(円)", fontsize=9)
ax01.yaxis.set_major_formatter(plt.FuncFormatter(lambda x, _: f"{x/1e6:.0f}M"))
ax01.grid(axis="y", alpha=0.3)
ax01.tick_params(axis="x", labelsize=8)

# [1,0] 散布図: 単価 vs 数量
ax10 = axes[1, 0]
for cat, clr in zip(CAT_NAMES, COLORS):
    sub = df100.filter(pl.col("product_category") == cat)
    ax10.scatter(sub["unit_price"].to_numpy(), sub["quantity"].to_numpy(),
                 label=cat, color=clr, alpha=0.5, s=25, edgecolors="none")
ax10.set_title("③ 単価 vs 販売数量", fontsize=11, pad=8)
ax10.set_xlabel("単価(円)", fontsize=9)
ax10.set_ylabel("数量(個)", fontsize=9)
ax10.xaxis.set_major_formatter(plt.FuncFormatter(lambda x, _: f"{x/1e3:.0f}K"))
ax10.legend(fontsize=7, loc="upper right")
ax10.grid(alpha=0.2)

# [1,1] 相関係数ヒートマップ(4変数)
ax11 = axes[1, 1]
num_cols100 = ["quantity", "unit_price", "sales_amount", "is_returned"]
col_lbl100  = ["数量", "単価", "売上", "返品"]
mat100 = np.column_stack([df100[c].to_numpy() for c in num_cols100])
c100   = np.corrcoef(mat100, rowvar=False)
im100  = ax11.imshow(c100, cmap="RdBu_r", vmin=-1, vmax=1)
for i in range(4):
    for j in range(4):
        fc = "white" if abs(c100[i, j]) > 0.6 else "black"
        ax11.text(j, i, f"{c100[i,j]:.2f}", ha="center", va="center",
                  fontsize=9, color=fc, fontweight="bold")
ax11.set_xticks(range(4)); ax11.set_xticklabels(col_lbl100, fontsize=9)
ax11.set_yticks(range(4)); ax11.set_yticklabels(col_lbl100, fontsize=9)
ax11.set_title("④ 相関係数ヒートマップ", fontsize=11, pad=8)
plt.colorbar(im100, ax=ax11, shrink=0.7)

plt.tight_layout()
plt.savefig("no100_annual_dashboard.svg", format="svg", bbox_inches="tight")
plt.show()
print("保存完了: no100_annual_dashboard.svg")
findfont: Failed to find font weight bold, now using 400.


findfont: Failed to find font weight bold, now using 400.


svg

保存完了: no100_annual_dashboard.svg
# ── Step 4: テキストレポート出力 ──
divider = "=" * 54
print(divider)
print("  産業用センサー 2024年度 年次販売分析レポート")
print(divider)
print(f"  分析対象期間: 2024年1月 〜 2024年12月")
print(f"  総取引件数  : {n_rows:>8,} 件")
print(f"  総売上金額  : {total_sales:>12,} 円")
print(f"  総販売数量  : {total_qty:>8,} 個")
print(f"  全体返品率  : {return_rate:>8.2f} %")
print(divider)
print()
print("【製品カテゴリ別 売上ランキング】")
for rank, (cat, sales) in enumerate(zip(
        cat100["product_category"].to_list(),
        cat100["cat_sales"].to_list()), 1):
    pct = sales / total_sales * 100
    print(f"  {rank}{cat:10s}: {sales:>12,} 円  ({pct:.1f}%)")
print()
print("【顧客タイプ別 売上ランキング】")
for rank, (cust, sales) in enumerate(zip(
        cust100["customer_type"].to_list(),
        cust100["cust_sales"].to_list()), 1):
    pct = sales / total_sales * 100
    print(f"  {rank}{cust:8s}: {sales:>12,} 円  ({pct:.1f}%)")
print()
print("【TOP 3 製品モデル】")
for rank, (prod, sales) in enumerate(zip(
        top3_products["product_name"].to_list(),
        top3_products["prod_sales"].to_list()), 1):
    print(f"  {rank}{prod:6s}: {sales:>12,} 円")
print()
best_month = monthly100.sort("monthly_sales", descending=True).head(1)
worst_month = monthly100.sort("monthly_sales").head(1)
print(f"【月次売上ハイライト】")
print(f"  最大月: {best_month['year_month'][0]}  {best_month['monthly_sales'][0]:>12,} 円")
print(f"  最小月: {worst_month['year_month'][0]}  {worst_month['monthly_sales'][0]:>12,} 円")
print(f"  最大/最小比: {best_month['monthly_sales'][0] / worst_month['monthly_sales'][0]:.2f} 倍")
print()
print(divider)
print("  以上")
print(divider)
======================================================
  産業用センサー 2024年度 年次販売分析レポート
======================================================
  分析対象期間: 2024年1月 〜 2024年12月
  総取引件数  :      301 件
  総売上金額  :  239,354,914 円
  総販売数量  :   11,048 個
  全体返品率  :     3.32 %
======================================================

【製品カテゴリ別 売上ランキング】
  1位 流量センサー    :   64,369,428 円  (26.9%)
  2位 温度センサー    :   60,772,674 円  (25.4%)
  3位 振動センサー    :   41,410,765 円  (17.3%)
  4位 圧力センサー    :   41,210,887 円  (17.2%)
  5位 電流センサー    :   31,591,160 円  (13.2%)

【顧客タイプ別 売上ランキング】
  1位 自動車     :   84,512,570 円  (35.3%)
  2位 半導体     :   53,920,928 円  (22.5%)
  3位 食品      :   48,413,908 円  (20.2%)
  4位 医療      :   36,645,931 円  (15.3%)
  5位 一般産業    :   15,861,577 円  (6.6%)

【TOP 3 製品モデル】
  1位 TS-A  :   20,271,994 円
  2位 FS-S  :   19,761,553 円
  3位 FS-A  :   19,456,558 円

【月次売上ハイライト】
  最大月: 2024-09    25,273,346 円
  最小月: 2024-04    15,582,567 円
  最大/最小比: 1.62 倍

======================================================
  以上
======================================================

結果の読み取り

No.100 ミニ分析レポートから読み取れる製造業への示唆:

  1. 年間売上の月次分布(ダッシュボード①)
    最大月と最小月の倍率が大きいほど季節性が強く、
    生産・在庫・調達計画を月単位で精緻に立てる必要があります。

  2. 製品カテゴリ別売上構成(ダッシュボード②)
    上位2カテゴリで売上の過半を占める場合(パレートの法則)、
    その製品の需要変動が業績全体に直結するリスクがあります。

  3. 単価 vs 数量の散布図(ダッシュボード③)
    カテゴリ間のクラスタが明確に分かれており、
    製品タイプごとに異なる価格戦略が必要であることを示しています。

  4. 相関係数ヒートマップ(ダッシュボード④)
    単価と数量の相関が明確な場合、価格改定の影響を事前に定量化できます。
    返品との相関が弱い場合は、品質以外の要因(仕様ミス・誤発注)を調査すべきです。


🎉 100本ノックシリーズ完走おめでとうございます!
No.001 から No.100 まで、Python の基礎から Polars によるデータ分析・可視化まで、
製造業の実務課題を題材に一通りの Python データ分析スキルを習得しました。


対象ノックを通して見える実務上の示唆

No.091〜100 の最終章を通して、製造業データ分析における以下の示唆が得られます。

1. 集計の粒度設計が意思決定の質を決める

group_by で何を軸に集計するかは、問いの設計 そのものです。
「どの製品が売れているか」という問いには製品別集計、
「どの顧客が最も価値が高いか」には顧客別集計が必要です。
データ分析の出発点は「何を知りたいか」を明確にすることです。

2. 日付型の正規化はすべての時系列分析の前提

日付型の変換を怠ると、集計結果のソートが文字列順になり
グラフや表が時系列として崩れます。
CSV 読み込み直後に必ず型変換を行うを鉄則にしましょう。

3. 可視化は「誰が見るか」で選択する

  • 折れ線グラフ: 時系列推移(管理職向け月次報告)
  • 棒グラフ: カテゴリ比較(製品別・顧客別ランキング)
  • ヒストグラム: 分布の確認(品質管理・異常検知)
  • 散布図: 2変数の関係(価格戦略・設備効率の評価)

4. 相関は因果ではない

r=0.8r = -0.8(強い負の相関)があっても、
「価格を下げれば必ず数量が増える」という結論は正しくありません。
相関分析は「調査すべき仮説の候補」を提示するものであり、
因果関係の確認には A/B テスト・準実験・業務プロセスの知見が必要です。

5. 分析の再現性がDXの基盤

np.random.seed(42) で乱数を固定し、コードを notebook に記録することで、
1年後に同じ手順で分析を再実行できます
これが「担当者が変わっても業務が止まらない」DX の出発点です。

実務導入する場合に必要なこと

Python データ分析を製造現場で本格活用するためのチェックリスト

1. 環境整備

  • Python 環境(venv or conda)を全担当者のPCに統一
  • requirements.txt で Polars・NumPy・Matplotlib のバージョンを固定
  • Git でノートブックをバージョン管理(変更履歴を追跡)

2. データパイプラインの設計

  • 生産管理システムからの CSV エクスポート形式を確認・標準化
  • 列名・データ型・欠損値の表現を社内で統一(データ辞書の作成)
  • 月次・年次の定期実行スクリプトを用意(コピペ作業の排除)

3. 可視化・レポートの標準化

  • 社内報告用グラフのカラーパレット・フォントを統一
  • KPI の定義(売上金額・返品率・成長率の計算式)を文書化
  • ダッシュボード用 notebook テンプレートを作成

4. チームへの展開

  • 本シリーズ(No.001〜100)を社内 Python 研修テキストとして活用
  • 分析担当者とエンジニアの共通言語として Polars + matplotlib を導入
  • 分析結果の解釈・意思決定ルールを業務手順書に追記

まとめ

本章(No.091〜100)では、Polars + matplotlib による集計・可視化・相関分析を
産業用センサーメーカーの年次販売実績データを題材に学びました。

No.学んだこと製造業での活用
091group_by集計製品カテゴリ・顧客タイプ別の年間売上集計
092商品別集計ABC 分析による主力製品・廃番候補の特定
093日付型変換月次集計・時系列分析の前提条件
094月別集計月次推移・前月比成長率・四半期別集計
095折れ線グラフ月次売上・数量推移の視覚化
096棒グラフカテゴリ別・顧客別売上比較
097ヒストグラム取引金額・単価の分布と外れ値検出
098散布図単価 vs 数量の価格弾力性の視覚化
099相関係数KPI間の関係強度を相関係数行列で定量化
100ミニ分析レポート集計・グラフ・テキストの一括出力パイプライン

100本ノック全章完走まとめ:

範囲テーマ
1章No.001〜010Pythonの準備と基本
2章No.011〜020変数・型・演算
3章No.021〜030文字列・リスト・辞書
4章No.031〜040条件分岐と繰り返し
5章No.041〜050関数・モジュール・例外処理
6章No.051〜060ファイル操作とCSV
7章No.061〜070NumPy入門
8章No.071〜080Polars入門
9章No.081〜090Polarsによるデータ加工
10章No.091〜100集計・可視化・ミニ分析

法人向けのご相談

製造業の DX 推進・データ分析基盤の構築・社内 Python 研修については、
数理工房にお気軽にご相談ください。

📩 お問い合わせ: surikobo.co.jp/contact
まずはお気軽にご相談ください。


本記事は「データ分析のための Python 入門 100本ノック」シリーズの最終記事です。
シリーズ全体の目次は こちら からご確認ください。