Pythonで美しく機能的なWebアプリを!「Dash」で始めるデータ分析ダッシュボード構築

投稿者: | 2026-07-18

こんにちは。皆さんは、データの分析結果を社内やチームに共有する際、どのようにプレゼンテーションしていますか?Excelのグラフや静的なレポートも素敵ですが、ユーザーが自由に条件を変更して探索できる「動的なWeb画面」があると、より深い洞察が得られますよね。今回は、Pythonだけでリッチな分析ダッシュボードや業務Webアプリを構築できる強力なライブラリ「Dash」をご紹介します。フロントエンドの専門知識がなくても、データサイエンスの成果をそのまま美しいWebアプリへと昇華させることができます。

Dashとは?

Dashは、データ視覚化ライブラリであるPlotly社によって開発された、データ分析に特化したWebアプリケーションフレームワークです。Flask、Plotly.js、そしてReact.jsをベースに構築されていますが、開発者はそれらの複雑な仕組みを意識することなく、PythonコードのみでインタラクティブなWeb画面を作り上げることができます。

Dashを導入するメリット

  1. Pythonだけで完結するフルスタック開発
    HTML、CSS、JavaScriptなどのWebフロントエンド言語を一切書くことなく、コンポーネントを配置していくだけでモダンなUI(ユーザーインターフェース)が完成します。
  2. 直感的かつ高度なインタラクティブ性
    ドロップダウンの選択やスライダーの移動、グラフ上のクリックといったユーザーの操作に応じて、リアルタイムに表示データを変更・再描画する「コールバック(Callback)」機能が極めて直感的で強力です。

HTMLやJavaScriptを個別に勉強しなくても、Pythonのロジックだけで高機能な画面が構築できるのは本当に魅力的ですよね。

  1. 豊富なビジュアライゼーション
    Plotlyの表現力豊かなグラフをそのまま埋め込むことができるため、3Dプロット、地図データ、金融チャートなど、あらゆるデータの可視化に対応できます。

実装サンプル:動的な棒グラフの作成

グラフが動的に変化するダッシュボードのコード例です。

ローカル環境で実行するだけで、ローカルサーバーが立ち上がり、ブラウザ上で動くダッシュボードがすぐに完成します!

pip install dash plotly pandas
import random

import dash
from dash import Input, Output, dash_table, dcc, html
import pandas as pd
import plotly.express as px


# =========================================================
# サンプルデータの作成
# =========================================================

random.seed(42)

regions = ["東日本", "西日本", "中部", "九州"]
products = ["ノートPC", "モニター", "周辺機器", "ソフトウェア"]
years = [2024, 2025, 2026]

base_sales = {
    "ノートPC": 1_500_000,
    "モニター": 900_000,
    "周辺機器": 550_000,
    "ソフトウェア": 700_000,
}

region_rate = {
    "東日本": 1.25,
    "西日本": 1.10,
    "中部": 0.95,
    "九州": 0.80,
}

records = []

for year in years:
    year_rate = 1 + ((year - 2024) * 0.08)

    for month in range(1, 13):
        # 年末に売上が伸びるような季節変動
        seasonal_rate = 1 + (month - 6) * 0.015

        for region in regions:
            for product in products:
                variation = random.uniform(0.80, 1.20)

                sales = int(
                    base_sales[product]
                    * region_rate[region]
                    * year_rate
                    * seasonal_rate
                    * variation
                )

                profit_rate = random.uniform(0.15, 0.28)
                profit = int(sales * profit_rate)

                average_order_price = {
                    "ノートPC": 150_000,
                    "モニター": 55_000,
                    "周辺機器": 18_000,
                    "ソフトウェア": 70_000,
                }[product]

                orders = max(
                    1,
                    int(
                        sales
                        / average_order_price
                        * random.uniform(0.85, 1.15)
                    ),
                )

                records.append(
                    {
                        "Year": year,
                        "Month": month,
                        "Date": pd.Timestamp(year=year, month=month, day=1),
                        "Region": region,
                        "Product": product,
                        "Sales": sales,
                        "Profit": profit,
                        "Orders": orders,
                    }
                )

df = pd.DataFrame(records)

df["MonthLabel"] = df["Date"].dt.strftime("%Y年%m月")


# =========================================================
# Dashアプリの初期化
# =========================================================

app = dash.Dash(__name__)

app.title = "営業売上ダッシュボード"


# =========================================================
# 共通スタイル
# =========================================================

PAGE_STYLE = {
    "backgroundColor": "#f4f7fb",
    "minHeight": "100vh",
    "padding": "28px",
    "fontFamily": (
        "'Yu Gothic UI', 'Meiryo', "
        "'Hiragino Kaku Gothic ProN', sans-serif"
    ),
}

CARD_STYLE = {
    "backgroundColor": "#ffffff",
    "borderRadius": "14px",
    "padding": "20px",
    "boxShadow": "0 4px 16px rgba(0, 0, 0, 0.06)",
    "border": "1px solid #e8edf5",
}

KPI_CARD_STYLE = {
    **CARD_STYLE,
    "flex": "1",
    "minWidth": "210px",
}

KPI_LABEL_STYLE = {
    "fontSize": "14px",
    "color": "#6b7280",
    "marginBottom": "8px",
}

KPI_VALUE_STYLE = {
    "fontSize": "30px",
    "fontWeight": "bold",
    "color": "#1f2937",
    "margin": "0",
}


# =========================================================
# レイアウト
# =========================================================

app.layout = html.Div(
    style=PAGE_STYLE,
    children=[
        # ヘッダー
        html.Div(
            [
                html.H1(
                    "営業売上ダッシュボード",
                    style={
                        "margin": "0",
                        "fontSize": "32px",
                        "color": "#1f2937",
                    },
                ),
                html.P(
                    "地域・商品カテゴリ・年度ごとの売上状況を確認できます。",
                    style={
                        "marginTop": "8px",
                        "marginBottom": "0",
                        "color": "#6b7280",
                    },
                ),
            ],
            style={"marginBottom": "24px"},
        ),

        # フィルター
        html.Div(
            style={
                **CARD_STYLE,
                "marginBottom": "22px",
            },
            children=[
                html.Div(
                    [
                        html.Div(
                            [
                                html.Label(
                                    "年度",
                                    style={
                                        "display": "block",
                                        "fontWeight": "bold",
                                        "marginBottom": "8px",
                                        "color": "#374151",
                                    },
                                ),
                                dcc.Dropdown(
                                    id="year-dropdown",
                                    options=[
                                        {"label": f"{year}年", "value": year}
                                        for year in years
                                    ],
                                    value=2026,
                                    clearable=False,
                                ),
                            ],
                            style={
                                "flex": "1",
                                "minWidth": "180px",
                            },
                        ),
                        html.Div(
                            [
                                html.Label(
                                    "地域",
                                    style={
                                        "display": "block",
                                        "fontWeight": "bold",
                                        "marginBottom": "8px",
                                        "color": "#374151",
                                    },
                                ),
                                dcc.Dropdown(
                                    id="region-dropdown",
                                    options=[
                                        {"label": region, "value": region}
                                        for region in regions
                                    ],
                                    value=regions,
                                    multi=True,
                                    placeholder="地域を選択",
                                ),
                            ],
                            style={
                                "flex": "2",
                                "minWidth": "260px",
                            },
                        ),
                        html.Div(
                            [
                                html.Label(
                                    "商品カテゴリ",
                                    style={
                                        "display": "block",
                                        "fontWeight": "bold",
                                        "marginBottom": "8px",
                                        "color": "#374151",
                                    },
                                ),
                                dcc.Dropdown(
                                    id="product-dropdown",
                                    options=[
                                        {
                                            "label": product,
                                            "value": product,
                                        }
                                        for product in products
                                    ],
                                    value=products,
                                    multi=True,
                                    placeholder="商品カテゴリを選択",
                                ),
                            ],
                            style={
                                "flex": "2",
                                "minWidth": "260px",
                            },
                        ),
                    ],
                    style={
                        "display": "flex",
                        "gap": "18px",
                        "flexWrap": "wrap",
                    },
                )
            ],
        ),

        # KPIカード
        html.Div(
            [
                html.Div(
                    [
                        html.Div("総売上", style=KPI_LABEL_STYLE),
                        html.P(id="sales-kpi", style=KPI_VALUE_STYLE),
                    ],
                    style=KPI_CARD_STYLE,
                ),
                html.Div(
                    [
                        html.Div("総利益", style=KPI_LABEL_STYLE),
                        html.P(id="profit-kpi", style=KPI_VALUE_STYLE),
                    ],
                    style=KPI_CARD_STYLE,
                ),
                html.Div(
                    [
                        html.Div("受注件数", style=KPI_LABEL_STYLE),
                        html.P(id="orders-kpi", style=KPI_VALUE_STYLE),
                    ],
                    style=KPI_CARD_STYLE,
                ),
                html.Div(
                    [
                        html.Div("利益率", style=KPI_LABEL_STYLE),
                        html.P(id="margin-kpi", style=KPI_VALUE_STYLE),
                    ],
                    style=KPI_CARD_STYLE,
                ),
            ],
            style={
                "display": "flex",
                "gap": "18px",
                "flexWrap": "wrap",
                "marginBottom": "22px",
            },
        ),

        # 上段グラフ
        html.Div(
            [
                html.Div(
                    dcc.Graph(
                        id="sales-line-chart",
                        config={"displayModeBar": False},
                    ),
                    style={
                        **CARD_STYLE,
                        "flex": "2",
                        "minWidth": "520px",
                    },
                ),
                html.Div(
                    dcc.Graph(
                        id="region-pie-chart",
                        config={"displayModeBar": False},
                    ),
                    style={
                        **CARD_STYLE,
                        "flex": "1",
                        "minWidth": "360px",
                    },
                ),
            ],
            style={
                "display": "flex",
                "gap": "18px",
                "flexWrap": "wrap",
                "marginBottom": "22px",
            },
        ),

        # 商品別グラフ
        html.Div(
            dcc.Graph(
                id="product-bar-chart",
                config={"displayModeBar": False},
            ),
            style={
                **CARD_STYLE,
                "marginBottom": "22px",
            },
        ),

        # データテーブル
        html.Div(
            [
                html.H2(
                    "月別・地域別データ",
                    style={
                        "fontSize": "20px",
                        "color": "#1f2937",
                        "marginTop": "0",
                    },
                ),
                dash_table.DataTable(
                    id="sales-table",
                    columns=[
                        {"name": "年月", "id": "MonthLabel"},
                        {"name": "地域", "id": "Region"},
                        {"name": "商品", "id": "Product"},
                        {
                            "name": "売上",
                            "id": "Sales",
                            "type": "numeric",
                            "format": {
                                "specifier": ",",
                                "locale": {
                                    "symbol": ["¥", ""],
                                },
                            },
                        },
                        {
                            "name": "利益",
                            "id": "Profit",
                            "type": "numeric",
                            "format": {
                                "specifier": ",",
                                "locale": {
                                    "symbol": ["¥", ""],
                                },
                            },
                        },
                        {
                            "name": "受注件数",
                            "id": "Orders",
                            "type": "numeric",
                            "format": {"specifier": ","},
                        },
                    ],
                    page_size=12,
                    sort_action="native",
                    filter_action="native",
                    style_table={
                        "overflowX": "auto",
                        "borderRadius": "10px",
                    },
                    style_header={
                        "backgroundColor": "#eef2f7",
                        "fontWeight": "bold",
                        "color": "#374151",
                        "border": "none",
                        "padding": "12px",
                    },
                    style_cell={
                        "fontFamily": "'Yu Gothic UI', 'Meiryo', sans-serif",
                        "textAlign": "left",
                        "padding": "11px",
                        "border": "none",
                        "borderBottom": "1px solid #edf0f4",
                        "color": "#374151",
                        "backgroundColor": "#ffffff",
                    },
                    style_data_conditional=[
                        {
                            "if": {"row_index": "odd"},
                            "backgroundColor": "#fafbfc",
                        },
                        {
                            "if": {"column_id": ["Sales", "Profit", "Orders"]},
                            "textAlign": "right",
                        },
                    ],
                ),
            ],
            style=CARD_STYLE,
        ),
    ],
)


# =========================================================
# コールバック
# =========================================================

@app.callback(
    Output("sales-kpi", "children"),
    Output("profit-kpi", "children"),
    Output("orders-kpi", "children"),
    Output("margin-kpi", "children"),
    Output("sales-line-chart", "figure"),
    Output("region-pie-chart", "figure"),
    Output("product-bar-chart", "figure"),
    Output("sales-table", "data"),
    Input("year-dropdown", "value"),
    Input("region-dropdown", "value"),
    Input("product-dropdown", "value"),
)
def update_dashboard(selected_year, selected_regions, selected_products):
    # 未選択の場合でもエラーにしない
    selected_regions = selected_regions or []
    selected_products = selected_products or []

    filtered_df = df[
        (df["Year"] == selected_year)
        & (df["Region"].isin(selected_regions))
        & (df["Product"].isin(selected_products))
    ].copy()

    total_sales = filtered_df["Sales"].sum()
    total_profit = filtered_df["Profit"].sum()
    total_orders = filtered_df["Orders"].sum()

    profit_margin = (
        total_profit / total_sales * 100
        if total_sales > 0
        else 0
    )

    sales_kpi = f"¥{total_sales:,.0f}"
    profit_kpi = f"¥{total_profit:,.0f}"
    orders_kpi = f"{total_orders:,.0f}件"
    margin_kpi = f"{profit_margin:.1f}%"

    # 月別売上推移
    monthly_df = (
        filtered_df.groupby(["Month", "MonthLabel"], as_index=False)
        .agg(
            Sales=("Sales", "sum"),
            Profit=("Profit", "sum"),
        )
        .sort_values("Month")
    )

    line_fig = px.line(
        monthly_df,
        x="MonthLabel",
        y=["Sales", "Profit"],
        markers=True,
        title="月別 売上・利益推移",
        labels={
            "MonthLabel": "",
            "value": "金額",
            "variable": "項目",
        },
    )

    line_fig.update_layout(
        template="plotly_white",
        legend_title_text="",
        hovermode="x unified",
        margin={"l": 40, "r": 20, "t": 60, "b": 40},
        yaxis_tickprefix="¥",
        yaxis_tickformat=",",
        plot_bgcolor="#ffffff",
        paper_bgcolor="#ffffff",
    )

    line_fig.for_each_trace(
        lambda trace: trace.update(
            name={
                "Sales": "売上",
                "Profit": "利益",
            }.get(trace.name, trace.name)
        )
    )

    # 地域別売上構成
    region_df = (
        filtered_df.groupby("Region", as_index=False)
        .agg(Sales=("Sales", "sum"))
        .sort_values("Sales", ascending=False)
    )

    pie_fig = px.pie(
        region_df,
        names="Region",
        values="Sales",
        hole=0.52,
        title="地域別 売上構成",
    )

    pie_fig.update_traces(
        textposition="inside",
        textinfo="percent+label",
        hovertemplate=(
            "%{label}<br>"
            "売上: ¥%{value:,.0f}<br>"
            "構成比: %{percent}"
            "<extra></extra>"
        ),
    )

    pie_fig.update_layout(
        template="plotly_white",
        showlegend=False,
        margin={"l": 20, "r": 20, "t": 60, "b": 20},
        paper_bgcolor="#ffffff",
    )

    # 商品別売上・利益
    product_df = (
        filtered_df.groupby("Product", as_index=False)
        .agg(
            Sales=("Sales", "sum"),
            Profit=("Profit", "sum"),
        )
        .sort_values("Sales", ascending=False)
    )

    bar_fig = px.bar(
        product_df,
        x="Product",
        y=["Sales", "Profit"],
        barmode="group",
        title="商品カテゴリ別 売上・利益",
        labels={
            "Product": "",
            "value": "金額",
            "variable": "項目",
        },
    )

    bar_fig.update_layout(
        template="plotly_white",
        legend_title_text="",
        margin={"l": 40, "r": 20, "t": 60, "b": 40},
        yaxis_tickprefix="¥",
        yaxis_tickformat=",",
        plot_bgcolor="#ffffff",
        paper_bgcolor="#ffffff",
    )

    bar_fig.for_each_trace(
        lambda trace: trace.update(
            name={
                "Sales": "売上",
                "Profit": "利益",
            }.get(trace.name, trace.name)
        )
    )

    # テーブル表示用データ
    table_df = filtered_df[
        [
            "Date",
            "MonthLabel",
            "Region",
            "Product",
            "Sales",
            "Profit",
            "Orders",
        ]
    ].sort_values(
        ["Date", "Region", "Product"],
        ascending=[False, True, True],
    )

    table_data = table_df.drop(columns=["Date"]).to_dict("records")

    return (
        sales_kpi,
        profit_kpi,
        orders_kpi,
        margin_kpi,
        line_fig,
        pie_fig,
        bar_fig,
        table_data,
    )


# =========================================================
# サーバー起動
# =========================================================

if __name__ == "__main__":
    app.run(debug=True)

みーちゃんのワンポイント

Dashを使う上で最も重要なのは「状態(State)とコールバック(Callback)の設計」です。複数の入力が複雑に絡み合うとアプリの挙動が重くなったり、意図しない無限ループが発生したりすることがあります。最初に入力と出力の依存関係をシンプルに整理してから実装を始めることが、美しく軽快なアプリを仕上げる最大のコツですよ。