INVESTMENT #07

Matplotlibで円グラフを作る(ポートフォリオのセクター分散)

Python経験者向けの投資分析シリーズ第7回。保有銘柄の評価額をセクター別の円グラフにします。pie() の使い方に加え、3.11で変わった戻り値欠損で合計が静かに狂う落とし穴まで実行結果つきで扱います。

🎯 対象: Python経験者・投資分析 📦 matplotlib 3.11.1 / pandas 3.0.3 🧪 2026-08-31 に実行して確認 ⏱️ 読了: 約17分

1. この記事のゴールと連載の位置づけ

本記事は「Python経験者が、自分の投資ポートフォリオを管理・可視化するアプリを作る」連載の第7回です。第4回のPythonで保有銘柄の損益を計算する(pandas)で、銘柄ごとの評価額(market_value)が並んだ損益一覧まで作りました。第5回・第6回はその数字を時系列のグラフにしてきましたが、今回は角度を変えます。「いま持っているものが、どんな種類に偏っているのか」を1枚で見る円グラフです。

銘柄名が並んだ表は、増えるほど読めなくなります。20行の表を眺めて「自分は何に賭けているのか」を言葉にできる人は多くありません。同じデータでもセクター(業種)でまとめて円グラフにすると、構成が一目で言えるようになります。今回作る部品は第13回のダッシュボードにそのまま載せる想定です。

円グラフは pie() を1行呼べば描けます。この記事が扱うのはその先——カテゴリが多くて潰れる/小さいスライスのラベルが重なる/データが欠損しているという、手を動かすと必ず出る問題です。

🎯
この回のゴール

銘柄ごとの評価額が入った DataFrame を渡すと、セクター別の構成比をドーナツグラフで返す関数を書けるようになること。セクター情報が取れない銘柄(ETFなど)が混ざっていても、合計が狂わない作りにします。

完成コードだけ先に見たい方は 7章(ネットワーク不要でそのまま動く形)。pie() の書き方だけ見たい方は 4章からどうぞ。

🧪
この記事の検証環境

本文のコード・出力・画像は、Windows 11 / Python 3.12.10 / matplotlib 3.11.1 / yfinance 1.4.1 / pandas 3.0.3 / NumPy 2.4.6 の環境で 2026年8月31日に実際に実行して確認したものです。掲載している図は、載せているコードを実行して出てきたそのままの画像です。

保有数量はすべて架空です。銘柄コードと株価は実在のものを使っていますが、取得した日時点の実行例であり、推奨銘柄でも、運用実績でもありません。

2. 材料をそろえる(評価額とセクター)

円グラフの材料は「カテゴリ」と「数値」の2列だけ。今回はカテゴリがセクター、数値が評価額です。

第4回の損益一覧は架空の識別子(ALPHA・BETA・GAMMA)で作りました。今回はセクターを実際に取得したいので、ティッカーを実在の銘柄コードに置き換えます。数量は引き続き架空です。評価額は「保有数量 × 現在値」で、現在値の取得は第1回のyfinanceで株価データを取得すると同じやり方です。

import pandas as pd
import yfinance as yf

# 保有数量はすべて架空。銘柄コードは実在のもの(分散の様子を作るため業種を散らしてある)
holdings = {
    "7203.T": 300, "6758.T": 200, "8306.T": 400, "9432.T": 3000,
    "4568.T": 100, "6501.T": 100, "9983.T": 10, "1306.T": 500,
}

def fetch_close(ticker):
    """直近の終値を1つ返す(第1回で作った取得処理の簡易版)。"""
    hist = yf.Ticker(ticker).history(period="5d", auto_adjust=False)
    return float(hist["Close"].iloc[-1])

positions = pd.DataFrame(
    [{"ticker": t, "qty": q, "price": fetch_close(t)} for t, q in holdings.items()]
)
positions["market_value"] = positions["qty"] * positions["price"]
print(positions.to_string(index=False))
print("\n評価額の合計:", f"{positions['market_value'].sum():,.0f} 円")
ticker  qty   price  market_value
7203.T  300  3156.0      946800.0
6758.T  200  4016.0      803200.0
8306.T  400  3680.0     1472000.0
9432.T 3000   172.5      517500.0
4568.T  100  2848.0      284800.0
6501.T  100  5438.0      543800.0
9983.T   10 72450.0      724500.0
1306.T  500   433.5      216750.0

評価額の合計: 5,509,350 円

8銘柄で約550万円ぶんの架空ポートフォリオができました。最後の 1306.T だけはETF——これが3章のトラブルメーカーです。

セクターは yfinance から取る(詳細は番外編へ)

カテゴリ側は Ticker.infosector から取れます。ただしこの取得はそれ自体が落とし穴の塊——ETFでは KeyError になり、返る分類名はGICSではなく、ETFの中身を見るには別のAPIが要ります。長くなるので番外編yfinanceでセクター情報を取得するに切り出しました。ここでは結論だけ使います。

def fetch_sector(ticker):
    info = yf.Ticker(ticker).info
    return {
        "ticker": ticker,
        "quote_type": info.get("quoteType"),   # ⚠️ info["sector"] だと ETF で KeyError
        "sector": info.get("sector"),
    }

sectors = pd.DataFrame([fetch_sector(t) for t in holdings])
print(sectors.to_string(index=False))
ticker quote_type                 sector
7203.T     EQUITY      Consumer Cyclical
6758.T     EQUITY             Technology
8306.T     EQUITY     Financial Services
9432.T     EQUITY Communication Services
4568.T     EQUITY             Healthcare
6501.T     EQUITY            Industrials
9983.T     EQUITY      Consumer Cyclical
1306.T        ETF                    NaN

7銘柄はセクターが取れ、ETFの行だけ NaN。返る分類名が英語なのは仕様で、日本語にするには自分で対応表を持ちます(7章の完成コードに載せてあります)。この NaN をどう扱うかが次章のテーマです。

🔁
株価は毎日変わるので、以降は値を固定して進めます

上の price2026年8月31日に取得した終値なので、同じコードを実行すればちがう値になります。図と数字を突き合わせられるように、以降はここで得た評価額を固定値として埋め込みます。自分のツールにするときは fetch_close() を呼ぶ形に戻してください。

なお yfinance はPyPIの説明で「調査・教育目的を意図している」「Yahoo! finance の API は個人利用のみを意図している」と明示しています。取得した値の再配布や業務利用の前に、必ず自分で最新の利用条件を確認してください(番外編のyfinanceの利用条件にもまとめています)。

3. つまずき実演:欠損で集計が静かに狂う

集計します。ここでいちばん怖いバグが出ます。

sector_map = dict(zip(sectors["ticker"], sectors["sector"]))
positions["sector"] = positions["ticker"].map(sector_map)

by_sector = positions.groupby("sector")["market_value"].sum().sort_values(ascending=False)
print(by_sector.to_string())
print("\n円グラフに渡す合計:", f"{by_sector.sum():,.0f} 円")
print("実際の評価額合計 :", f"{positions['market_value'].sum():,.0f} 円")
sector
Consumer Cyclical         1671300.0
Financial Services        1472000.0
Technology                 803200.0
Industrials                543800.0
Communication Services     517500.0
Healthcare                 284800.0

円グラフに渡す合計: 5,292,600 円
実際の評価額合計 : 5,509,350 円

21万6,750円が消えました。ETF1本ぶんです。エラーは出ません。警告も出ません。groupby() はグループキーが NaN の行を既定で除外するので、黙って集計から落ちるのです。

円グラフは「全体を100%として構成比を見る」図です。そこに渡す全体が、実は全体ではなかった——これは例外で止まってくれるバグより、はるかに厄介です。グラフはきれいに描けてしまい、パーセンテージも合計100%になり、見た目には何の異常もありません。集計したら合計を検算する、というひと手間だけが防波堤になります。

もう1つの壊れ方:ValueError で落ちる

「表示順を毎回同じにしたい」と考えて並び順を固定すると、今度は例外になります。

import matplotlib.pyplot as plt      # 描画はここが初出(4章でも同じものを使います)

# ⚠️ 失敗例:保有していないセクターが NaN になり、pie() に渡せない
ORDER = ["Technology", "Financial Services", "Industrials", "Consumer Cyclical",
         "Communication Services", "Healthcare", "Energy"]
ordered = by_sector.reindex(ORDER)

fig, ax = plt.subplots()
ax.pie(ordered, labels=ordered.index)
ValueError: Wedge sizes must be finite numbers

reindex() は指定した並びに存在しない項目を NaN で埋めます。エネルギーセクターを持っていないので NaN になり、pie() が例外を投げました。並び順を固定するなら .reindex(ORDER).fillna(0) と、欠けた枠を0で埋めてから渡します(0のスライスは描画されません)。

🧯
pie() が投げるエラーの早見表(3.11.1で実測)
渡した値出るもの
[30.0, nan, 50.0]ValueError: Wedge sizes must be finite numbers
[30, None, 50]TypeError: '<' not supported between instances of 'NoneType' and 'int'
[30, -5, 50]ValueError: Wedge sizes 'x' must be non negative values

NaNNone出る例外の種類が違うのが地味な落とし穴です。エラーメッセージの読み解き方はPythonのよくあるエラーと対処法にまとめてあります。

欠損を捨てずにカテゴリへ寄せる

対処は単純で、集計する前に欠損を埋めるだけ。ETFを「ETF・投資信託」という自分用のカテゴリとして立てます。

positions["sector"] = positions["sector"].fillna("ETF・投資信託")
by_sector = positions.groupby("sector")["market_value"].sum().sort_values(ascending=False)
print(by_sector.to_string())
print("合計:", f"{by_sector.sum():,.0f} 円")
sector
Consumer Cyclical         1671300.0
Financial Services        1472000.0
Technology                 803200.0
Industrials                543800.0
Communication Services     517500.0
Healthcare                 284800.0
ETF・投資信託                   216750.0
合計: 5,509,350 円

合計がぴったり 5,509,350円に戻りました。「器」であるETFを1カテゴリとして扱う形です。もう一つ、ETFの中身を11セクターに分解して足し込む道もあり、こちらは番外編yfinanceでセクター情報を取得するで扱っています。中身まで見たいのか、器の単位で管理したいのか——目的で選ぶもので、どちらが正解ということはありません。以降はこの集計結果を使って描画に進みます。

4. Matplotlibで円グラフを描く(pie の基本)

描いてみます。呼び出しはこれだけです。

import matplotlib.pyplot as plt

fig, ax = plt.subplots(figsize=(6, 6))
ax.pie(by_sector, labels=by_sector.index)
ax.set_title("Portfolio by sector")
fig.savefig("sector_basic.png", dpi=100, bbox_inches="tight")
Matplotlibの既定設定で描いたセクター別の円グラフ。英語のセクター名は正しく表示されているが、日本語のカテゴリ名「ETF・投資信託」だけが□(豆腐)に文字化けしている
▲ 円にはなった。ただし右下のラベルが□に化けている

円グラフにはなりました。ただし日本語で付けた「ETF・投資信託」だけが □ に化けています。実行時にはこんな警告が出ていました。

Glyph 12539 (\N{KATAKANA MIDDLE DOT}) missing from font(s) DejaVu Sans.
Glyph 25237 (\N{CJK UNIFIED IDEOGRAPH-6295}) missing from font(s) DejaVu Sans.
(同種の警告が計10件)

Matplotlibの既定フォント(DejaVu Sans)が日本語のグリフを持っていないためで、エラーにならず絵は出てしまうのが厄介なところです。対処は第5回のMatplotlibのグラフで日本語が文字化けして□になるときの対処で手順を通しています。要点は「自分の環境に実在する日本語フォント名を調べて font.family に指定する」の一点です。

フォント・パーセント・開始角度を整える

文字化けを直し、最低限ほしい要素を足します。

SECTOR_JA = {          # yfinance が返す英語名 → 表示用の日本語(全11種類は7章に)
    "Consumer Cyclical": "一般消費財", "Financial Services": "金融",
    "Technology": "テクノロジー", "Industrials": "資本財・産業",
    "Communication Services": "通信サービス", "Healthcare": "ヘルスケア",
}
labels_ja = [SECTOR_JA.get(s, s) for s in by_sector.index]   # 自作カテゴリはそのまま

with plt.rc_context({"font.family": "Meiryo"}):    # 以降のコードに設定を持ち越さない
    fig, ax = plt.subplots(figsize=(6, 6))
    ax.pie(by_sector, labels=labels_ja, autopct="%1.1f%%",
           startangle=90, counterclock=False)
    ax.set_title("セクター別の評価額(架空のポートフォリオ)")
    fig.savefig("sector_jp.png", dpi=100, bbox_inches="tight")
日本語フォントを指定し、パーセント表示と開始角度を整えたセクター別円グラフ。一般消費財30.3%、金融26.7%、テクノロジー14.6%などが時計回りに並んでいる
▲ 日本語も出た。ただし上部の小さいスライスはラベルが窮屈になっている

足した引数は3つだけですが、意味は大きいです。

引数既定値何が変わるか
autopct="%1.1f%%"None各スライスに構成比を書き込む。書式は fmt % pct で処理されるため %% でパーセント記号1つ
startangle=900最初のスライスの開始位置。既定は3時方向で、90にすると12時から始まる
counterclock=FalseTrue並ぶ向き。既定は反時計回りなので、時計回りにしたいなら明示が必要
explode=[0.06, 0, 0, 0, 0, 0, 0]None指定したスライスだけ中心から離して強調する。値は半径に対する比率で、データと同じ長さのリストが要る(違うと ValueError: 'explode' must be of length 'x')。全部離すと面積を比べにくい

startangle=90counterclock=False はセットで覚えると楽です。2つ合わせて「12時から時計回り」になり、大きい順に並べたデータなら人が読む順序と一致します。

🚫
引数は位置ではなくキーワードで渡す

ax.pie(sizes, explode_list, labels) のように位置で渡すと MatplotlibDeprecationWarning: Passing the explode parameter of pie() positionally is deprecated since Matplotlib 3.10; the parameter will become keyword-only in 3.12. が出ます。3.12で書けなくなる予定なので、最初からキーワード指定にしておいてください。

5. Matplotlib 3.11で pie() の戻り値が変わった

ネット上のサンプルを写経すると、ここで説明と実物が食い違います。多くの記事は「pie() はタプルを返す」と書きますが、3.11から返るのは PieContainer というオブジェクトです。検証環境の matplotlib 3.11.1Axes.pie の docstring(matplotlib/axes/_axes.py)にも versionchanged:: 3.11 として「以前はタプルで返していた」と明記されています(公式リファレンス)。

fig, ax = plt.subplots(figsize=(6, 6))
result = ax.pie(by_sector, labels=labels_ja, autopct="%1.1f%%")

print(type(result))
print("wedges:", len(result.wedges))
print("values:", result.values)
print("fracs :", result.fracs.round(4))
<class 'matplotlib.container.PieContainer'>
wedges: 7
values: [1671300. 1472000.  803200.  543800.  517500.  284800.  216750.]
fracs : [0.3034 0.2672 0.1458 0.0987 0.0939 0.0517 0.0393]

これは実は便利になっています。values(元の数値)と fracs(構成比)を後から取り出せるので、凡例に構成比を書くとき自分で割り算し直す必要がありません

互換性はどうかというと、従来のアンパックはそのまま動きます

wedges, texts, autotexts = ax.pie(by_sector, labels=labels_ja, autopct="%1.1f%%")
print(len(wedges), len(texts), len(autotexts))
print(type(wedges[0]).__name__, type(texts[0]).__name__, type(autotexts[0]).__name__)
7 7 7
Wedge Text Text

既存のコードは書き換え不要です。ただしタプルのつもりで扱うと転ぶ場所が2つあります。

try:
    len(result)
except TypeError as e:
    print("len():", e)

print("result.texts:", type(result.texts).__name__,
      "外側", len(result.texts), "/内側", [len(x) for x in result.texts])
len(): object of type 'PieContainer' has no len()
result.texts: list 外側 2 /内側 [7, 7]

1つめ、len() が使えませんTypeError)。戻り値の要素数で分岐しているコードは書き換えが必要です。

2つめ、アンパックで受け取る texts と、属性の result.texts は別物です。属性のほうは「ラベルの一覧」と「autopctの一覧」が入った入れ子のリスト(外側2・内側7ずつ)。for t in result.texts: t.set_fontsize(9) と書くと、テキストではなくリストを操作しようとして失敗します。

📌
バージョンを書き留めておく理由

PieContainer のdocstring(matplotlib 3.11.1・matplotlib/container.py)には「このクラス名は暫定的で、今後変わる可能性がある」と書かれています。いま動くコードが将来も同じ形とは限らないので、自分のツールには使った matplotlib のバージョンをコメントで残しておくと、数年後の自分が助かります。

6. 読める円グラフにする(集約・ドーナツ・凡例)

4章の図は小さいスライスのラベルが窮屈でした。円グラフが読みにくくなる原因はほぼ2つ——カテゴリが多すぎることと、ラベルを円の中に詰め込みすぎることです。順に潰します。

上位N+「その他」にまとめる

11分類がすべて出てくると細いスライスが並んで読めません。上位いくつかを残して残りを合算するのが定石です。

def top_n_with_others(s, n=5, other_label="その他"):
    """上位n件を残し、残りを1つに合算する(合計は変わらない)。"""
    s = s.sort_values(ascending=False)
    if len(s) <= n:
        return s
    return pd.concat([s.iloc[:n], pd.Series({other_label: s.iloc[n:].sum()})])

print(top_n_with_others(by_sector, n=5).to_string())
Consumer Cyclical         1671300.0
Financial Services        1472000.0
Technology                 803200.0
Industrials                543800.0
Communication Services     517500.0
その他                        501550.0

ポイントは合算であって切り捨てではないことです。円グラフで全体が全体でなくなるのは致命的なので、減らすときは必ず「その他」に足します

ドーナツにして、実額は凡例に逃がす

ラベルの窮屈さは円の中に置く情報を減らすと解決します。スライスにはパーセントだけを置き、セクター名と実額は凡例へ。ドーナツ形にすると中央が空くので、そこに合計を置けます。

total = float(by_sector.sum())
labels = [f"{SECTOR_JA.get(s, s)} {v:,.0f}円" for s, v in by_sector.items()]

with plt.rc_context({"font.family": "Meiryo"}):
    fig, ax = plt.subplots(figsize=(7.4, 5.4))
    result = ax.pie(
        by_sector,
        autopct="%1.1f%%",
        startangle=90,
        counterclock=False,
        pctdistance=0.79,
        wedgeprops={"width": 0.42, "edgecolor": "white", "linewidth": 1.5},
        textprops={"fontsize": 9},
    )
    ax.legend(result.wedges, labels, title="セクター(評価額)",
              loc="center left", bbox_to_anchor=(1.0, 0.5), frameon=False)
    ax.text(0, 0, f"合計\n{total:,.0f}円", ha="center", va="center", fontsize=11)
    ax.set_title("セクター別の評価額(架空のポートフォリオ)")
ドーナツ形にしたセクター別評価額のグラフ。スライスにはパーセントのみを表示し、右側の凡例にセクター名と実額、中央に合計5,509,350円を表示している
▲ 完成形。円の中はパーセントだけにして、名前と金額は凡例へ

効いている引数は次の3つです。

引数効果
wedgeprops={"width": 0.42}ドーナツになる。各スライスの図形(Wedge)に渡され、外周から内側0.42ぶんだけを描く。中央が空くので合計を置ける
wedgeprops={"edgecolor": "white", "linewidth": 1.5}スライスの境界に白い線が入り、隣り合う色が似ていても切れ目が分かる
pctdistance=0.79パーセント文字の位置(中心からの相対距離)。既定の0.6のままだとドーナツの穴に文字が落ちる

ドーナツ化に専用の引数はありません。wedgepropswidth を渡すだけ。

カテゴリをさらに増やすと1%未満のスライスが並び、今度は数字が団子になります。そのときは autopct関数を渡し、閾値未満は空文字を返して黙らせると読めます(実額は凡例に残るので情報は失われません)。渡した関数はパーセント値を引数に呼ばれ、返した文字列がそのまま描かれるので、出し分けは自由です。

7. 完成コード(関数にまとめる)

ここまでを2つの関数にまとめます。集計する関数描く関数を分けたのは、集計結果を表として見たい場面(Streamlitの表など)が来るからです。ネットワークなしで動くよう入力は固定値にしています。

"""セクター別の資産配分をドーナツグラフにする。
検証環境: Python 3.12.10 / matplotlib 3.11.1 / pandas 3.0.3(2026-08-31 実行)
"""
import matplotlib.pyplot as plt
import pandas as pd

SECTOR_JA = {
    "Basic Materials": "素材",
    "Communication Services": "通信サービス",
    "Consumer Cyclical": "一般消費財",
    "Consumer Defensive": "生活必需品",
    "Energy": "エネルギー",
    "Financial Services": "金融",
    "Healthcare": "ヘルスケア",
    "Industrials": "資本財・産業",
    "Real Estate": "不動産",
    "Technology": "テクノロジー",
    "Utilities": "公益",
}


def aggregate_by_sector(positions, fallback="ETF・投資信託", top_n=None):
    """評価額をセクター別に集計する。sector が欠損した行は fallback にまとめる。"""
    df = positions.copy()
    df["sector"] = df["sector"].fillna(fallback)        # 欠損を捨てない(合計が狂わない)
    s = df.groupby("sector")["market_value"].sum().sort_values(ascending=False)
    if top_n is not None and len(s) > top_n:
        s = pd.concat([s.iloc[:top_n], pd.Series({"その他": s.iloc[top_n:].sum()})])
    return s


def plot_sector_pie(by_sector, title="セクター別の評価額", font="Meiryo",
                    donut=True, savepath=None):
    """セクター別評価額の Series を円グラフ(既定はドーナツ)にする。"""
    total = float(by_sector.sum())
    labels = [f"{SECTOR_JA.get(s, s)} {v:,.0f}円" for s, v in by_sector.items()]
    wedgeprops = {"edgecolor": "white", "linewidth": 1.5}
    if donut:
        wedgeprops["width"] = 0.42

    with plt.rc_context({"font.family": font}):
        fig, ax = plt.subplots(figsize=(7.4, 5.4))
        result = ax.pie(
            by_sector,
            autopct="%1.1f%%",
            startangle=90,
            counterclock=False,
            pctdistance=0.79 if donut else 0.62,
            wedgeprops=wedgeprops,
            textprops={"fontsize": 9},
        )
        ax.legend(result.wedges, labels, title="セクター(評価額)",
                  loc="center left", bbox_to_anchor=(1.0, 0.5), frameon=False)
        if donut:
            ax.text(0, 0, f"合計\n{total:,.0f}円", ha="center", va="center", fontsize=11)
        ax.set_title(title)
        if savepath:
            fig.savefig(savepath, dpi=100, bbox_inches="tight")
        return fig, result


if __name__ == "__main__":
    # 第4回の損益一覧と同じ形。数量は架空、終値は2026-08-31時点の実行例
    positions = pd.DataFrame(
        [
            ("7203.T", 300, 3156.0, "Consumer Cyclical"),
            ("6758.T", 200, 4016.0, "Technology"),
            ("8306.T", 400, 3680.0, "Financial Services"),
            ("9432.T", 3000, 172.5, "Communication Services"),
            ("4568.T", 100, 2848.0, "Healthcare"),
            ("6501.T", 100, 5438.0, "Industrials"),
            ("9983.T", 10, 72450.0, "Consumer Cyclical"),
            ("1306.T", 500, 433.5, None),          # ETF は sector が取れない
        ],
        columns=["ticker", "qty", "price", "sector"],
    )
    positions["market_value"] = positions["qty"] * positions["price"]

    by_sector = aggregate_by_sector(positions)
    print(by_sector.to_string())
    print(f"\n合計: {by_sector.sum():,.0f} 円")

    fig, _ = plot_sector_pie(
        by_sector,
        title="セクター別の評価額(架空のポートフォリオ)",
        savepath="sector_pie.png",
    )
    plt.close(fig)
sector
Consumer Cyclical         1671300.0
Financial Services        1472000.0
Technology                 803200.0
Industrials                543800.0
Communication Services     517500.0
Healthcare                 284800.0
ETF・投資信託                   216750.0

合計: 5,509,350 円

出力される sector_pie.png は、6章に載せた完成形の図と同じものです。実データにつなぐときは positions の作り方だけを差し替えます。数量と価格は第4回の損益一覧から、sector は番外編yfinanceでセクター情報を取得するfetch_sector_info() から取ります。描画側の関数には手を入れずに済みます。

top_n=5 で上位5+その他に、donut=False で普通の円グラフになります。見せ方を呼び出し側で選べるようにしておくと、ダッシュボードに組み込むときそのまま使えます。

8. つまずきやすいポイントと次につなげる

架空データで動いても、自分のポートフォリオを流し込むと別の問題が出ます。先に共有しておきます。

つまずき何が起きるか対処
欠損のまま groupby例外は出ず、合計が静かに減るfillna() でカテゴリに寄せる。集計後に合計を検算する
並び順を reindex で固定ValueError: Wedge sizes must be finite numbers.fillna(0) を続ける。0のスライスは描かれない
戻り値を len() で数える3.11から TypeErrorresult.wedges の長さを見る
カテゴリが多いまま描くラベルが重なって読めない上位N+その他に合算し、実額は凡例へ

とくに1行目はグラフが正常に見えてしまうぶん、気づく機会がありません。集計関数の最後に「入力の合計と出力の合計が一致するか」を確かめる1行を入れておくと、この種の事故はまとめて防げます。CSVの読み込みで詰まったら第3回の証券会社のCSVをpandasで読み込むが近道で、引数の網羅的な確認は公式の Axes.pie リファレンスが確実です。他のライブラリと合わせて全体像をつかむならPythonライブラリ一覧もどうぞ。

📖
次に読むなら:pandasの集計を土台から

今回いちばん危なかったのは pie() の書き方ではなく、groupby が欠損を黙って落とすという pandas の挙動のほうでした。描画の前段——結合・集計・欠損の扱い——を一度通しで押さえておくと、この記事の8章の表を引かなくても自分で気づけるようになります。Pythonによるデータ分析入門 第3版は pandas の開発者本人が書いた定番書で、読み込みから集計・可視化までを手を動かしながら学べます。当サイトのおすすめ本ランキングでは総合5位として紹介しており、リンク先では対象レベル・全612ページというボリューム・出版社の公式ページを確認でき、そこから楽天の価格・在庫ページへ進めます。なお、リンク先は当サイトの書籍紹介ページで、アフィリエイト広告を含みますのでご了承ください。

次につなげる:可視化の部はここまで

第5回のローソク足、第6回の対話的なグラフ、そして今回の円グラフで、連載の可視化フェーズ(第5〜7回)は一区切りです。いずれも第4回の損益一覧をそのまま材料にできます。続く第8回が配当金管理ツールを作る(tkinter × SQLite)——今度はデータを貯める側で、配当をGUIで入力してSQLiteに記録し、銘柄別・月別に集計します。連載の全体像と公開済みの記事は投資×Python シリーズ一覧から辿れます。

📌
この連載のスタンス

本連載は「自分の資産を自分で管理・可視化するツールを、Pythonの学習題材として自作する」ことが目的です。特定銘柄の売買や投資手法を勧めるものではなく、記事中の保有数量はすべて描画方法を示すための架空例です。当サイトは投資助言業(金融商品取引業)の登録を行っておりません。グラフの読み方や個別の投資判断についてのご質問にはお答えできません。投資判断はご自身の責任で行ってください(詳細は免責事項をご覧ください)。

9. よくある質問(FAQ)

Matplotlibで円グラフを描くときにつまずきやすい点をまとめました。

Q. 円グラフのラベルが□に文字化けします。どう直せばいいですか?

Matplotlibの既定フォント(DejaVu Sans)が日本語のグリフを持っていないためで、Glyph 12539 ... missing from font(s) DejaVu Sans. という警告が出ます。plt.rcParams["font.family"] = "Meiryo" のように日本語を含むフォントを指定すれば直ります。使える名前は環境ごとに違うので、matplotlib.font_manager.fontManager.ttflist で実在する名前を確認してください(検証環境の Windows 11 では MeiryoYu GothicMS Gothic が使えました)。手順は第5回の日本語が文字化けするときの対処にあります。

Q. 円グラフにパーセントと実際の金額を両方表示できますか?

できます。autopct には書式文字列だけでなく関数も渡せます。関数はパーセント値を受け取るので、合計を掛け戻せば金額に復元できる仕組みです。autopct=lambda pct: f"{pct:.1f}%\n{total * pct / 100:,.0f}円" と書けば2行で表示されます。ただしスライスが小さいと文字が重なります。本記事では実額を凡例に逃がし、スライスにはパーセントだけを置きました(6章)。

Q. スライスの順番や向きを固定したいです。

順番は渡すデータの並び順そのままなので sort_values(ascending=False) で並べてから渡します。向きは startangle=90(12時から開始)と counterclock=False(時計回り)の組み合わせが読みやすいでしょう。既定は「3時から反時計回り」で、指定しないと直感と逆になります。reindex() で順序を固定するなら、該当データが無い項目が NaN になり ValueError で落ちるため .fillna(0) を続けてください。

Q. 色を自分で指定できますか? 実行のたびに色が変わって困ります。

colors 引数に色のリストを渡せば固定できます。指定しないとその時点のカラーサイクルが使われるため、カテゴリが増減すると同じセクターの色が変わります。時系列で比較する資料なら、セクター名と色の対応表を持ち colors=[color_map[s] for s in by_sector.index] と渡します。

Q. ドーナツグラフにするための引数はどれですか?

専用の引数はありません。wedgeprops={"width": 0.42} のようにスライスの幅を指定すると内側がくり抜かれ、ドーナツになります(widthmatplotlib.patches.Wedge に渡され、外半径から内側にその幅ぶんだけを描きます)。中央が空くので ax.text(0, 0, ...) で合計値を置けます。パーセントの文字が穴に落ちる場合は pctdistance を0.75〜0.8に上げてください。

Q. wedges, texts, autotexts = ax.pie(...) は今でも動きますか?

動きます。ただしMatplotlib 3.11で戻り値はタプルから PieContainer オブジェクトに変わりました(公式docstringに versionchanged:: 3.11 の記載あり)。アンパックは後方互換で残っていますが、len() は使えず TypeError: object of type 'PieContainer' has no len() になります。属性 result.texts入れ子のリストでアンパック時の texts とは形が違うので、要素数は len(result.wedges) で数えてください。

Q. 円グラフの合計が、実際の評価額と合いません。

groupby()グループキーの欠損した行を既定で除外するのが原因のことが多いです。セクターが取れない銘柄(ETFなど)が混ざるとその行が黙って集計から落ち、例外も警告も出ないまま構成比だけが100%になります。集計前に fillna("ETF・投資信託") でカテゴリを与えるか、groupby(..., dropna=False) を使ってください。取得側の事情は番外編yfinanceでセクター情報を取得するにまとめています。

Q. 円グラフで見たときにセクターが偏っていたら、リスクが高いということですか?

この記事はその判断には踏み込みません。円グラフが示すのは「いま自分の評価額がどう配分されているか」という事実であって、それが良いか悪いかの評価ではありません。同じ配分でも、目的・投資期間・他に持っている資産・その人の事情によって意味はまったく変わります。当サイトは投資助言業の登録を行っておらず、配分の是非についてはお答えできません。この記事の目的は、判断材料を自分の手で正しく作れるようになること——とくに3章で見たように、集計が静かに狂ったまま気づかない状態を避けることです。

🧰
読み終えたら:次に手を動かす3本

7章のコードを sector_pie.py として保存しておけば、連載の後半でそのまま再利用できます。いま地続きで手を動かせるのは次の3本です。

連載の全体像と公開済みの記事は投資×Python シリーズ一覧にまとめてあります。続きが出たときに気づけるよう、この一覧ページをブックマークしておくのが確実です。更新は新着記事のRSSでも配信しているので、RSSリーダーを使っているなら登録しておけば第8回の公開に自動で気づけます。

本記事は、生成AIを活用して下書きし、運営者が内容を確認・編集したうえで公開しています。掲載しているコード・出力・グラフ画像は Python 3.12.10 / matplotlib 3.11.1 / yfinance 1.4.1 / pandas 3.0.3 / NumPy 2.4.6 で実際に実行して確認したものです。銘柄コードと株価は実在のものですが保有数量はすべて架空で、取得した値は2026年8月31日時点の実行例です。本記事は描画技術の情報提供を目的としたもので、特定の銘柄の売買・投資手法・資産配分を推奨するものではありません。投資判断はご自身の責任で行ってください。AIの利用方針は免責事項をご覧ください。