1  質的データの出現頻度

Author

Ryo Nakagami

Published

2025-04-14

Modified

2025-04-16

問題設定

以下のようなカテゴリ別出現頻度のデータが与えられたとします.

  • faulty_component: 故障箇所
  • faulty_category: 故障内容に応じた分類
  • count: 出現頻度
Code
import pandas as pd


data = {
    "faulty_component": [
        "ハードディスク", "ハードディスク",
        "メモリ", "メモリ",
        "電源ユニット", "電源ユニット",
        "ファン", "マザーボード",
        "液晶ディスプレイ", "液晶ディスプレイ",
        "ソフトウェア", "ソフトウェア"
    ],
    "faulty_category": [
        "接続不良", "異音",
        "起動不良", "ブルースクリーン",
        "電源不安定", "電源不良",
        "異音", "接続不良",
        "画面不認識", "表示トラブル",
        "起動時エラー", "アプリ強制終了"
    ],
    "count": [125, 87, 73, 52, 64, 91, 40, 38, 70, 33, 49, 58]
}

df = pd.DataFrame(data)
df
faulty_component faulty_category count
0 ハードディスク 接続不良 125
1 ハードディスク 異音 87
2 メモリ 起動不良 73
3 メモリ ブルースクリーン 52
4 電源ユニット 電源不安定 64
5 電源ユニット 電源不良 91
6 ファン 異音 40
7 マザーボード 接続不良 38
8 液晶ディスプレイ 画面不認識 70
9 液晶ディスプレイ 表示トラブル 33
10 ソフトウェア 起動時エラー 49
11 ソフトウェア アプリ強制終了 58

可視化方針

以下では

  1. 複数カテゴリ変数を結合して生成したラベル変数をkey,出現頻度をvalueとしたデータについて,出現頻度をbar plot, 累積頻度をline plotとして可視化
  2. 2つのカテゴリ次元にわたる値の頻度分布をheatmapを用いて可視化

の2つを可視化実装を取り扱いますが,実装説明の前にEDAとしてどのようなstory-tellingが考えられるか紹介します.

パレート図分析summary例

Example 1.1

分析テーマ 無数にあるように見える故障のうち,実際に対応すべきはごく一部の代表的なパターンなのではないか?
可視化解釈 カテゴリ組み合わせは無数にあるが,Top 10だけでも異常発生件数全体の 80%をしめており,Top 10のみに集中してアクションをとるべき
インサイト リソースを広く分散するのではなく,「ファン-動作不良」「HDD-認識不能」など,頻度の高いカテゴリに絞って改善策・品質対策・部品見直し・製造工程チェックを行うことが,最もコストパフォーマンスの高い改善になる


Example 1.2

分析テーマ 故障カテゴリの問題があるため故障頻度分布は裾の厚い分布担っているのではないか?
可視化解釈 カテゴリ組み合わせTop 10に絞っても累積故障割合は全体の40%を占めるにとどまっており,故障カテゴリは裾の厚い(fat-tailed)分布と見える.

故障カテゴリ分類について一部粒度が細かすぎるため裾が厚くなってしまったと仮説立て,補修アクションの観点を踏まえてカテゴリを再定義した上で集計し直したところ,Top10カテゴリで全体の約75%をカバーする構造が浮かび上がった.(=fat-tailed 分布の見え方は,分類設計そのものの歪みを示している可能性がある)
インサイト 故障ログの分類設計には,実際のオペレーションや対応フローとの整合性を持たせることが重要

再定義したカテゴリを標準分類とし,継続的な分析に活用できるよう設計を整備する必要性あり
heatmap分析summary例

Example 1.3

分析テーマ 故障箇所 × 故障内容の組み合わせ頻度に偏りが存在するかを確認し,重点対応すべきカテゴリを特定する
可視化解釈 故障箇所と故障内容の組み合わせを2軸にとったヒートマップを作成したところ,特定のパターンに件数が極端に集中している構造が明らかになった

たとえば,「HDD × 認識不能」や「ファン × 動作不良」といった一部の組み合わせセルが強調され,その他多くのセルが極端に件数の少ない状態であることから,故障パターンが限定的であることが視覚的に示された
インサイト 故障は一見バラバラに発生しているようでいて,実際には限られた数の典型的パターンに集中的に発生している


Example 1.4

分析テーマ 故障箇所 × 故障内容の組み合わせの中で,一見目立たないが相対的に異常頻度が高い“隠れホットスポット”を可視化する
可視化解釈 故障ログ全体をヒートマップで俯瞰した結果,全体件数では上位に入らないカテゴリの中にも,特定の故障内容に偏った異常発生が集中している箇所があることが明らかになった

「電源ユニット × 異音」や「メモリ × 接触不良」など,ランキングでは目立たないものの,他の故障内容と比較すると明らかに“浮いている”セルが存在していた
インサイト 今後のモニタリングでは,件数が少なくても極端な偏りを示す“異常に濃いセル”をトリガーに,点検対象の見直しやアラート条件の再設計を検討すべき

可視化実装1: パレート図

▶  前処理

出現頻度のカラムを用いて事前に descending sort を以下のように実施します.

Code
df_sorted = df.sort_values(by="count", ascending=False).reset_index(drop=True)
df_sorted["cum_percent"] = df_sorted["count"].cumsum() / df_sorted["count"].sum()
df_sorted["label"] = df_sorted["faulty_component"].astype(str) + "-" + df_sorted["faulty_category"].astype(str)
df_sorted.head()
faulty_component faulty_category count cum_percent label
0 ハードディスク 接続不良 125 0.160256 ハードディスク-接続不良
1 電源ユニット 電源不良 91 0.276923 電源ユニット-電源不良
2 ハードディスク 異音 87 0.388462 ハードディスク-異音
3 メモリ 起動不良 73 0.482051 メモリ-起動不良
4 液晶ディスプレイ 画面不認識 70 0.571795 液晶ディスプレイ-画面不認識


▶  引数テーブル

Args
引数 説明
data カテゴリ,件数,累積値を含む入力データ.cumulative plotが正しく動作するには,データが適切にソートされている必要がある
category_column x軸に使用するカテゴリ列の名前
count_column 棒グラフ(主軸)に使用する件数列の名前
cumulative_column line plot(y2-axis)に使用する累積値(例:パーセンテージ)の列の名前
xaxis_name : x軸のタイトル.デフォルトは ""
y1_axis_name y1-axisのタイトル.デフォルトは ""
y2_axis_name y2-axisのタイトル.デフォルトは ""
y1_label bar plotのlegendラベル.デフォルトは ""
y2_label cumulative plotのlegendラベル.デフォルトは ""
figure_title 図全体のタイトル.デフォルトは ""
figsize matplotlib用のfigure size設定値(tuple)


▶  Plotly を用いた可視化

Code
import plotly.graph_objects as go


def plot_category_frequency_plotly(
    data: pd.DataFrame,
    category_column: str,
    count_column: str,
    cumulative_column: str,
    xaxis_name: str = "",
    y1_axis_name: str = "",
    y2_axis_name: str = "",
    y1_label: str = "",
    y2_label: str = "",
    figure_title: str = "",
) -> go.Figure:
    """
    - カテゴリごとの件数を棒グラフで,累積値(例:累積パーセンテージ)を折れ線グラフで可視化するPlotly図を生成
    - cumulative plotが正しく動作するには,データが適切にソートされている必要がある
    """
    # create bar trace
    bar = go.Bar(
        x=data[category_column],
        y=data[count_column],
        name=y1_label,
        yaxis="y1",
        marker=dict(color="#6699CC"),
    )

    # Create cumulative line trace
    line = go.Scatter(
        x=data[category_column],
        y=data[cumulative_column],
        name=y2_label,
        yaxis="y2",
        mode="lines+markers",
        line=dict(color="#000000", dash="dot"),
    )

    # Layout
    layout = go.Layout(
        title=dict(
            text=figure_title,
            x=0.,
            xanchor="left",
            yanchor="top",
        ),
        margin=dict(t=50),
        xaxis=dict(title=xaxis_name, tickangle=45),
        yaxis=dict(title=y1_axis_name),
        yaxis2=dict(
            title=y2_axis_name,
            overlaying="y",
            side="right",
            range=[0, 1.1],
            gridcolor="#EFF5F5",
        ),
        legend=dict(x=1.05, y=1.0, orientation="v"),
    )

    # Plot
    fig = go.Figure(data=[bar, line], layout=layout).update_layout(
        {"plot_bgcolor": "#EFF5F5", "yaxis": {"gridcolor": "#EFF5F5"}}
    )

    return fig

実行コードは以下,

Code
fig = plot_category_frequency_plotly(
    data=df_sorted,
    category_column="label",
    count_column="count",
    cumulative_column="cum_percent",
    xaxis_name="Faulty Category",
    y1_axis_name="Frequency",
    y2_axis_name="Cumulative Percentage",
    y1_label="件数",
    y2_label="累積割合",
    figure_title="PC Faulty component frequency",
)
fig.show()


▶  matplotlib を用いた可視化

Code
import matplotlib.pyplot as plt


def plot_category_frequency_matplotlib(
    data: pd.DataFrame,
    category_column: str,
    count_column: str,
    cumulative_column: str,
    xaxis_name: str = "",
    y1_axis_name: str = "",
    y2_axis_name: str = "",
    y1_label: str = "",
    y2_label: str = "",
    figure_title: str = "",
    figsize: tuple = (8, 6),
):
    fig, ax1 = plt.subplots(figsize=figsize)
    ax1.set_facecolor("#EFF5F5")  # 👈 plot background color

    # Bar plot on primary y-axis
    bars = ax1.bar(
        data[category_column], data[count_column], color="#6699CC", label=y1_label
    )
    ax1.set_ylabel(y1_axis_name, fontsize=12)
    ax1.set_xlabel(xaxis_name, fontsize=12)
    ax1.tick_params(axis="y")

    # Line plot on secondary y-axis
    ax2 = ax1.twinx()
    line = ax2.plot(
        data[category_column],
        data[cumulative_column],
        color="#000000",
        linestyle="dotted",
        marker="o",
        label=y2_label,
    )
    ax2.set_ylabel(y2_axis_name, fontsize=12)
    ax2.set_ylim(0, 1.1)
    ax2.tick_params(axis="y")
    plt.setp(ax1.get_xticklabels(), rotation=(360-45), ha="left")  # 👈 Rotate x-axis labels
    
    # Title and legend
    fig.suptitle(figure_title, x=0.0, ha="left", fontsize=14)

    # Combine legends from both axes
    lines_labels = [*ax1.get_legend_handles_labels(), *ax2.get_legend_handles_labels()]
    fig.legend(loc="upper left", bbox_to_anchor=(0.925, 0.925))
    fig.patch.set_facecolor("#FFFFFF")

    plt.tight_layout()
    return fig, ax1, ax2

実行コードは以下,

Code
from regmonkey_style import stylewizard as sw

# japanese language setup
sw.set_font("IPAexGothic")


fig, ax1, ax2 = plot_category_frequency_matplotlib(
    data=df_sorted,
    category_column="label",
    count_column="count",
    cumulative_column="cum_percent",
    xaxis_name="Faulty Category",
    y1_axis_name="Frequency",
    y2_axis_name="Cumulative Percentage",
    y1_label="件数",
    y2_label="累積割合",
    figure_title="PC Faulty component frequency",
)
plt.show()

可視化実装2: heatmap

  • ここでは,xaxis, yaxisとなる2つのカテゴリ変数を用いてpivot tableを作成し,それをheatmapで可視化
  • pivotにあたっての集計関数は sum を用いる
  • NaN(欠損値)はプロット前に0で補完
  • "PuBu" カラースケールを使用

▶  実装

Code
import seaborn as sns


def heatmap_frequency(
    data: pd.DataFrame,
    xaxis_column: str,
    yaxis_column: str,
    value_column: str,
    x_axis_name: str = "",
    y_axis_name: str = "",
    figure_title: str = "",
    plot_type = 'plotly',
    figsize = (10, 6)
):
    table = pd.pivot_table(
        data,
        values=value_column,
        index=[yaxis_column],
        columns=[xaxis_column],
        aggfunc="sum",
    ).fillna(0)

    if plot_type == 'plotly':
        fig = go.Figure(
            data=go.Heatmap(
                z=table.values,
                x=table.columns,  # Optional: x-axis labels
                y=table.index,  # Optional: y-axis labels
                colorscale="PuBu",  # You can try: 'Blues', 'Hot', 'Cividis', etc.
            )
        )

        fig.update_layout(
            title=figure_title, xaxis_title=x_axis_name, yaxis_title=y_axis_name
            
        )

        return fig
    
    else:
         # Create the plot
        fig, ax = plt.subplots(figsize=figsize)

        sns.heatmap(
            table.values,
            xticklabels=table.columns,
            yticklabels=table.index,
            cmap="PuBu",
            ax=ax
        )

        ax.set_xlabel(x_axis_name)
        ax.set_ylabel(y_axis_name)
        ax.set_title(figure_title, loc="center")

        plt.xticks(rotation=(360-45), ha='left')
        plt.yticks(rotation=0)

        plt.tight_layout()
        return fig, ax

▶  Plotly による可視化

Code
fig = heatmap_frequency(
    data=df_sorted,
    xaxis_column="faulty_category",
    yaxis_column="faulty_component",
    value_column="count",
    x_axis_name="故障カテゴリ",
    y_axis_name="故障箇所",
    figure_title=dict(
        text="故障分布",
        font=dict(size=18),
        automargin=True,
        x=0.5,  # Center title
        y=0.95,
        xanchor="center",
    ),
)


fig.show()

▶  matplotlib による可視化

Code
fig, ax = heatmap_frequency(
    data=df_sorted,
    xaxis_column='faulty_category',
    yaxis_column='faulty_component',
    value_column='count',
    x_axis_name='故障カテゴリ',
    y_axis_name='故障箇所',
    figure_title='故障分布',
    plot_type = 'matplotlib',
)

plt.show()