経費のCSVを渡されても、月別と部門別を同時に見たい瞬間に表計算ソフトでは手が止まりがちです。そこでPythonのStreamlitとPlotlyを組み合わせ、CSVを読み込むだけで月別・部門別・カテゴリ別を切り替えられる経費分析ダッシュボードを作りました。
実際にCSV経費データをアップロードして分析を更新したところ、合計経費のKPIカードと月別推移サマリーが画面に表示されています。この記事では読み込みから前処理、フィルター、集計までを断片ごとに分解して解説します。
Streamlitの基本概念、要件定義、実装、動作確認までを順番に学べる構成です。動画は目次から確認したい場面へ移動でき、本文だけでも手順と考え方が完結します。
動画の内容をテキストで確認する
オープニング。StreamlitとPlotlyを使って経費分析ダッシュボードを作るカリキュラムを始めます。概要紹介。
StreamlitとPlotlyの役割と使い方を学ぶ 経費分析ダッシュボードの要件を整理する 完成コードと実行結果を確認する 最後に完成した画面を実際に操作して確かめる 具体的にやること。
st.file_uploaderでCSV経費データを受け取るutf-8-sigとcp932を順に試して読み込むdate・department・category・amountの列の有無を検証する 日付と金額を型変換して欠損行を除外するdate列からYYYY-MM形式の月列を作る 実装環境・必須アプリ。
OS:Windows 11 Pro Python:3.13.3シェル:PowerShell 5.1必須アプリ:コードエディター、Webブラウザ、ターミナル パッケージ:pip、streamlit、plotly、pandas PythonのStreamlitとPlotlyとは。
Streamlit:StreamlitはPythonのスクリプトを上から実行するだけでブラウザ画面を組み立てられるWebアプリフレームワークPlotly:Plotly ExpressはDataFrameを渡すだけで拡大やホバーに対応した図を描けるグラフ描画ライブラリ StreamlitとPlotlyで作る経費分析画面の要点。
utf-8-sigとcp932を順に試して読み込む 不足した列名をそのままエラーへ含めるcoerce指定で変換できない値を欠損にする 経費分析ダッシュボードの要件定義。
CSV未選択時にアップロードを促す案内が出る 必要列が欠けたCSVで不足列名付きのエラーが出る 分析を更新を押すまで集計結果が出ない 更新後に合計経費と明細件数のKPIカードが並ぶ 月別推移の下に最大月と金額が出る 部門別比較とカテゴリ構成が左右に並ぶ INTRO: Monaco Editorで経費分析ダッシュボードを実装。
コードを1行ずつ入力し、補完と自動インデントを使いながら実行結果を確認します。LINE 001: モジュールの概要説明。このファイル全体がPlotlyとStreamlitで作る経費分析ダッシュボードであることを説明するドキュメント文字列です。
ファイルの先頭に書くことで、他の人が読んだときに用途をすぐ把握できます。LINE 003: 起動コマンドの記載。このスクリプトをStreamlitアプリとして起動するためのコマンドをコメントとして記載しています。
実際に動かす際はこの内容をターミナルに入力します。LINE 004: ドキュメント文字列の終わり。ファイル冒頭の説明文を閉じる記号です。
ここまでがモジュール全体の説明として扱われます。LINE 006: ioモジュールの読み込み。バイト列をファイルのように扱うための標準ライブラリioを読み込んでいます。
アップロードされたCSVデータをメモリ上で処理する際に使用します。LINE 008: pandasライブラリの読み込み。表形式データを扱うためのライブラリpandasをpdという名前で読み込んでいます。
CSVの読み込みや集計処理で使用します。LINE 009: Plotly Expressの読み込み。グラフを手軽に作成できるplotly.expressをpxという名前で読み込んでいます。
折れ線グラフや棒グラフの作成に使用します。LINE 010: Streamlitライブラリの読み込み。Webアプリの画面部品を作るためのStreamlitをstという名前で読み込んでいます。
画面表示や入力部品の作成に使用します。LINE 013: 確認対象ルートの定義。動作確認したい画面のパスをリストとして定義しています。
ここではトップページのみが対象になっています。LINE 015: 必須列名の定義。CSVに必ず含まれているべき列名をリストで定義しています。
この後の読み込み処理で列の過不足をチェックする際に使われます。LINE 016: カード用スタイルの開始。KPIカードの見た目を整えるためのCSS文字列の定義を開始しています。
複数行に分けて記述することで見やすくしています。LINE 017: カードスタイルの詳細1。カードの余白や枠線、角丸などのスタイルを指定している部分です。
CARD_STYLEという変数の一部として組み立てられています。LINE 018: カードスタイルの詳細2。カードの背景色や文字の太さを指定している部分です。
前の行と合わせてCARD_STYLE全体のデザインを完成させています。LINE 019: カードスタイル定義の終了。複数行にわたって組み立てたCARD_STYLE文字列の定義を閉じています。
これでKPIカードに使う統一デザインが変数として使えるようになります。LINE 020: セクション見出しスタイルの定義。各グラフの見出し部分に使う文字サイズや太さなどのスタイルをSECTION_STYLEとして定義しています。
後で複数の見出しに共通して使われます。LINE 021: 要約文スタイルの定義。グラフ下に表示する要約テキストのスタイルをSUMMARY_STYLEとして定義しています。
文字色や余白を指定して見やすく整えています。RUN 1/8: 共通の定数とスタイルをそろえる。必要な列名と、KPIカードや見出しに使う装飾を定義した段階です。
値を表示しておくと、後の関数がどの列を前提にしているかがはっきりします。CHECK 1/8: 途中実行に成功。必須列: ['date', 'department', 'category', 'amount'] 必須列数: 4 確認する画面: ['/'] RETURN 01: エディターへ戻りました。
入力済みのコードを維持したまま、次の実装へ進みます。LINE 025: CSV読み込み関数の定義開始。アップロードされたファイルを受け取り、pandasのDataFrameとして返す関数read_expense_csvを定義しています。
戻り値の型も明示しています。LINE 026: 関数の説明文。この関数がCSVを文字コードを判定しながら読み込むものであることを説明するドキュメント文字列です。
処理の目的をコード上に残しています。LINE 027: アップロードデータの取得。アップロードされたファイルオブジェクトから生のバイト列を取得しています。
この後の文字コード判定処理で使うための元データになります。LINE 028: 読み込み結果の初期化。CSV読み込み結果を格納する変数frameをNoneで初期化しています。
まだ読み込みに成功していない状態を表しています。LINE 029: 文字コード候補のループ開始。日本語CSVでよく使われるutf-8-sigとcp932の2つの文字コードを順番に試すループを開始しています。
どちらかで読み込めれば処理を続行します。LINE 030: 読み込み処理の例外対応開始。文字コードのエラーが発生してもプログラムが止まらないようにtry文で読み込み処理を囲んでいます。
LINE 031: CSVの読み込み実行。バイト列を指定した文字コードでCSVとして読み込み、DataFrameに変換しています。読み込みに成功すればframeにその結果が入ります。
LINE 032: ループの正常終了。CSVの読み込みに成功した場合、それ以上他の文字コードを試す必要がないためループを抜けています。LINE 033: 文字コードエラーの捕捉。
指定した文字コードで読み込めなかった場合に発生するUnicodeDecodeErrorを受け止めています。次の文字コードで再試行するための準備です。LINE 034: 読み込み失敗時のリセット。
文字コードの判定に失敗した場合、frameを再びNoneに戻しています。次の候補の文字コードで改めて読み込みを試みるためです。LINE 035: 全候補失敗の判定。
すべての文字コード候補で読み込みに失敗し、frameがNoneのままかどうかを確認しています。LINE 036: 文字コードエラーの通知。どの文字コードでも読み込めなかった場合に、利用者にわかりやすいエラーメッセージを添えて例外を発生させています。
LINE 038: 必須列の不足チェック。REQUIRED_COLUMNSの中で、読み込んだデータに存在しない列名を抽出してmissingというリストに格納しています。LINE 039: 不足列の有無判定。
missingリストに何か要素が入っているかどうか、つまり必須列が不足しているかどうかを確認しています。LINE 040: 不足列エラーの通知。必要な列が不足している場合に、不足している列名を含めたエラーメッセージとともに例外を発生させています。
LINE 041: 読み込み結果の返却。文字コード判定と列チェックを通過したDataFrameを呼び出し元に返しています。これで安全に読み込まれたデータが後続処理に渡ります。
RUN 2/8: CSVの読み込み関数を動かす。文字コードを試しながら読み込み、必要な列がそろっているかを検証する関数が完成した段階です。小さなCSVを渡して、列名と行数を確かめます。
CHECK 2/8: 途中実行に成功。読み込んだ列: ['date', 'department', 'category', 'amount'] 読み込んだ行数: 2 RETURN 02: エディターへ戻りました。入力済みのコードを維持したまま、次の実装へ進みます。
LINE 045: 前処理関数の定義開始。読み込んだデータの型変換や月列の追加を行う関数preprocessを定義しています。戻り値もDataFrame型であることを明示しています。
LINE 046: 前処理関数の説明文。この関数が日付と金額の型変換、集計用の月列の追加を行うことを説明するドキュメント文字列です。LINE 047: 必要列のみの抽出とコピー。
必須列だけを取り出して新しいDataFrameとしてtidyにコピーしています。元のデータを直接書き換えないようにするための処理です。LINE 048: 日付列の型変換。
date列の文字列を日付型に変換しています。変換できない値はエラーにせずコレース指定で欠損値として扱われます。LINE 049: 金額列の型変換。
amount列を数値型に変換しています。数値として解釈できない値は欠損値として扱われるように設定しています。LINE 050: 部門名の整形。
department列を文字列に統一し、前後の余分な空白を取り除いています。表記ゆれによる集計ミスを防ぐための処理です。LINE 051: カテゴリ名の整形。
category列を文字列に統一し、前後の空白を取り除いています。部門名と同様に集計の精度を上げるための処理です。LINE 052: 欠損データの除外。
date列またはamount列が欠損している行を取り除いています。集計に使えない不完全なデータをここで排除しています。LINE 053: 月列の作成。
date列から年月の文字列を作成し、month列として追加しています。この列が月別集計の軸として使われます。LINE 054: 日付順への並び替えと返却。
date列の順に並び替え、インデックスを振り直したうえで整形済みのデータを返しています。これで後続の集計処理に使いやすい形になります。RUN 3/8: 前処理で月列と型を整える。
日付と金額の型変換、空白の除去、月列の追加までが動くようになりました。部門名に空白を含めたデータを渡し、整形の結果を確かめます。CHECK 3/8: 途中実行に成功。
月列: ['2026-01', '2026-02'] 部門列: ['営業部', '開発部'] 金額の型: int64 RETURN 03: エディターへ戻りました。入力済みのコードを維持したまま、次の実装へ進みます。LINE 058: フィルター関数の定義開始。
月・部門・カテゴリの選択条件に応じて明細を絞り込む関数apply_filtersを定義しています。引数として選択済みの各条件を受け取ります。LINE 059: フィルター関数の説明文。
この関数が選択された条件で明細を絞り込み、未選択の軸は絞り込まないことを説明するドキュメント文字列です。LINE 060: 絞り込み対象の初期化。絞り込み処理の対象となる変数filteredに、まず全体のデータをそのまま代入しています。
ここから条件ごとに絞り込んでいきます。LINE 061: 月の選択有無の判定。月フィルターが何か選択されているかどうかを確認しています。
何も選ばれていない場合は月による絞り込みを行いません。LINE 062: 月による絞り込み。選択された月のリストに含まれる明細だけを残すように絞り込んでいます。
month列の値と選択内容を照合しています。LINE 063: 部門の選択有無の判定。部門フィルターが選択されているかどうかを確認しています。
選択がなければ部門による絞り込みは行いません。LINE 064: 部門による絞り込み。選択された部門のリストに含まれる明細だけを残すように絞り込んでいます。
department列の値と選択内容を照合しています。LINE 065: カテゴリの選択有無の判定。カテゴリフィルターが選択されているかどうかを確認しています。
選択がなければカテゴリによる絞り込みは行いません。LINE 066: カテゴリによる絞り込み。選択されたカテゴリのリストに含まれる明細だけを残すように絞り込んでいます。
category列の値と選択内容を照合しています。LINE 067: 絞り込み結果の返却。すべての条件を適用し終えたデータのインデックスを振り直して返しています。
これで画面表示や集計に使う最終的な絞り込み結果が得られます。RUN 4/8: フィルター関数で明細を絞る。月・部門・カテゴリの3軸で明細を絞り込む関数が動く状態です。
軸ごとに条件を変え、残る件数の違いを確かめます。CHECK 4/8: 途中実行に成功。全明細: 3件 2026-02だけ: 2件 営業部だけ: 2件 RETURN 04: エディターへ戻りました。
入力済みのコードを維持したまま、次の実装へ進みます。LINE 071: KPI集計関数の定義開始。絞り込み後のデータから合計金額などの指標をまとめて計算する関数build_kpiを定義しています。
戻り値は辞書型であることを明示しています。LINE 072: KPI集計関数の説明文。この関数が合計、件数、平均、最大の軸を集計することを説明するドキュメント文字列です。
LINE 073: 月別合計の集計。month列ごとにamountを合計し、月別の合計金額をby_monthとして計算しています。この結果は最大月の特定に使われます。
LINE 074: 部門別合計の集計。department列ごとにamountを合計し、部門別の合計金額をby_departmentとして計算しています。この結果は最大部門の特定に使われます。
LINE 075: カテゴリ別合計の集計。category列ごとにamountを合計し、カテゴリ別の合計金額をby_categoryとして計算しています。この結果は最大カテゴリの特定に使われます。
LINE 076: 全体合計金額の計算。絞り込み後のデータ全体のamountを合計し、float型のtotalとして計算しています。KPI表示の合計経費に使われます。
LINE 077: 集計結果辞書の作成開始。ここまでに計算した各種指標をまとめて1つの辞書として返す処理を開始しています。LINE 078: 合計金額の格納。
先ほど計算した合計金額totalを辞書のtotalキーに格納しています。画面のKPIカードに表示するための値です。LINE 079: 明細件数の格納。
絞り込み後のデータの行数を数え、整数として辞書のcountキーに格納しています。明細件数の表示に使われます。LINE 080: 平均金額の計算と格納。
合計金額を件数で割って1件あたりの平均金額を計算し、辞書のaverageキーに格納しています。LINE 081: 最大月の格納。月別合計の中で最も金額が大きい月の名前を取得し、辞書のtop_monthキーに格納しています。
LINE 082: 最大月の金額の格納。月別合計の中で最も大きい金額を取得し、辞書のtop_month_amountキーに格納しています。LINE 083: 最大部門の格納。
部門別合計の中で最も金額が大きい部門の名前を取得し、辞書のtop_departmentキーに格納しています。LINE 084: 最大部門の金額を計算。部門別の合計金額の中から最大値を取り出し、float型に変換して辞書に格納しています。
これによりKPI表示で最大部門の金額を数値として使えるようにしています。LINE 085: 最大カテゴリ名を取得。カテゴリ別の合計金額が最も大きいカテゴリ名をidxmaxで求め、文字列に変換しています。
カテゴリ構成の可視化で最大カテゴリを表示する際に使われます。LINE 086: 最大カテゴリの金額を計算。カテゴリ別の合計金額の最大値を取り出し、float型に変換しています。
KPIの一部として画面に金額を表示するために使われます。LINE 087: KPI辞書を返す。これまで計算した合計・件数・平均・各軸の最大値をまとめた辞書を関数の戻り値として返しています。
この辞書が画面表示の各KPIカードで利用されます。RUN 5/8: KPIの集計結果を取り出す。合計や件数、最大の月・部門・カテゴリを辞書へまとめる関数が完成しました。
画面に出す前の数値を、そのまま表示して確かめます。CHECK 5/8: 途中実行に成功。合計: 36000.0件数: 3 最大月: 2026-02 24000.0 最大部門: 営業部27000.0 RETURN 05: エディターへ戻りました。
入力済みのコードを維持したまま、次の実装へ進みます。LINE 091: 月別グラフ生成関数の定義。月ごとの経費推移を折れ線グラフとして作成する関数を定義しています。
引数には絞り込み後の明細データを受け取ります。LINE 092: 関数の説明文。この関数が月ごとの合計を折れ線グラフにする処理であることを説明しています。
関数の目的を明確にするためのドキュメント文字列です。LINE 093: 月別合計の集計。明細データを月でグループ化し、金額の合計を計算しています。
この集計結果がグラフのデータ元になります。LINE 094: 折れ線グラフの作成。月を横軸、金額を縦軸にした折れ線グラフをplotlyのlineで作成しています。
マーカー表示とタイトルも指定して見やすくしています。LINE 095: ホバー表示の設定。グラフにマウスを合わせたときに表示される情報を、月と金額をカンマ区切りの円表示に整えています。
利用者が値を読み取りやすくしています。LINE 096: 軸ラベルと高さの設定。横軸を「月」、縦軸を「経費(円)」というラベルに設定し、グラフの高さを360に指定しています。
見た目を整えるための調整です。LINE 097: 作成したグラフを返す。完成した折れ線グラフのオブジェクトを関数の戻り値として返しています。
呼び出し元の画面表示処理でこのグラフが使われます。RUN 6/8: 月別推移の折れ線グラフを作る。月ごとの合計を折れ線にするグラフ関数が動く状態です。
図の題名と、x軸とy軸に入った値を表示して中身を確かめます。CHECK 6/8: 途中実行に成功。グラフの題名: 月別経費の推移 月: ['2026-01', '2026-02'] 金額: [np.int64(12000), np.int64(24000)] RETURN 06: エディターへ戻りました。
入力済みのコードを維持したまま、次の実装へ進みます。LINE 101: 部門別グラフ生成関数の定義。部門ごとの経費を横棒グラフとして作成する関数を定義しています。
引数には絞り込み後の明細データを受け取ります。LINE 102: 関数の説明文。この関数が部門ごとの合計を横棒グラフにして可視化する処理であることを説明しています。
関数の目的を示すドキュメント文字列です。LINE 103: 部門別合計の集計開始。明細データを部門でグループ化し、金額を合計する処理の始まりです。
複数行にわたる処理をまとめて表現しています。LINE 104: 部門別合計を金額順に並べ替え。部門ごとの合計金額を計算した後、金額の小さい順に並べ替えています。
横棒グラフを見やすい順序で表示するための準備です。LINE 105: 集計結果の変数への代入完了。複数行にわたる部門別集計処理の結果を、department変数として確定させています。
この後のグラフ作成でこの変数が使われます。LINE 106: 横棒グラフの作成開始。plotlyのbarを使って部門別の横棒グラフを作成する処理を開始しています。
以降の引数で表示内容を細かく指定しています。LINE 107: グラフの元データ指定。横棒グラフに使うデータとして、先ほど集計したdepartmentのデータフレームを指定しています。
LINE 108: 横軸に金額を指定。グラフの横軸に金額の列を指定しています。横棒グラフでは棒の長さが金額の大きさを表します。
LINE 109: 縦軸に部門を指定。グラフの縦軸に部門名の列を指定しています。各部門ごとに1本の棒が並ぶように設定しています。
LINE 110: 横向きの棒グラフに設定。グラフの向きを横向きにする設定をしています。部門名が読みやすいように横棒グラフとして表示するための指定です。
LINE 111: 棒の上に数値を自動表示。各棒の上に金額の数値を自動で表示する設定をしています。利用者が一目で金額を確認できるようにしています。
LINE 112: グラフのタイトル設定。グラフの上部に表示するタイトルとして「部門別経費の比較」という文字列を指定しています。LINE 113: 棒グラフ作成の呼び出し終了。
px.barへの引数指定が終わり、横棒グラフのオブジェクトが作成される行です。複数行にまたがる関数呼び出しの終端です。LINE 114: 軸ラベルと高さの設定。
横軸を「経費(円)」、縦軸を「部門」というラベルに設定し、グラフの高さを360に指定しています。見た目を整えています。LINE 115: 作成したグラフを返す。
完成した横棒グラフのオブジェクトを関数の戻り値として返しています。呼び出し元の画面表示処理でこのグラフが使われます。RUN 7/8: 部門別比較の横棒グラフを作る。
部門ごとの合計を金額順に並べた横棒グラフができました。並び順と金額を表示し、意図した順序になっているかを確かめます。CHECK 7/8: 途中実行に成功。
グラフの題名: 部門別経費の比較 部門の並び: ['開発部', '営業部'] 金額: [np.int64(9000), np.int64(27000)] RETURN 07: エディターへ戻りました。入力済みのコードを維持したまま、次の実装へ進みます。LINE 119: カテゴリ別グラフ生成関数の定義。
カテゴリごとの経費をドーナツグラフとして作成する関数を定義しています。引数には絞り込み後の明細データを受け取ります。LINE 120: 関数の説明文。
この関数がカテゴリごとの合計をドーナツグラフにする処理であることを説明しています。関数の目的を示すドキュメント文字列です。LINE 121: カテゴリ別合計の集計。
明細データをカテゴリでグループ化し、金額の合計を計算しています。この集計結果がグラフのデータ元になります。LINE 122: ドーナツグラフの作成。
plotlyのpieを使い、カテゴリ名と金額をもとにドーナツグラフを作成しています。hole引数で中央に穴を開けドーナツ状にしています。LINE 123: グラフ内表示の設定。
グラフの各部分に割合とカテゴリ名を表示する設定をしています。利用者がひと目で構成比を把握できるようにしています。LINE 124: グラフの高さ設定。
ドーナツグラフ全体の高さを360に指定しています。他のグラフと表示サイズをそろえるための調整です。LINE 125: 作成したグラフを返す。
完成したドーナツグラフのオブジェクトを関数の戻り値として返しています。呼び出し元の画面表示処理でこのグラフが使われます。RUN 8/8: カテゴリ構成のドーナツグラフを作る。
カテゴリごとの合計を円形に描く関数まで用意できました。ラベルと金額を表示し、構成比のもとになる値を確かめます。CHECK 8/8: 途中実行に成功。
グラフの題名: カテゴリ構成 カテゴリ: ['交通費', '会議費'] 金額: [np.int64(12000), np.int64(24000)] RETURN 08: エディターへ戻りました。入力済みのコードを維持したまま、次の実装へ進みます。LINE 129: 画面全体の初期設定。
Streamlitの画面タイトルとレイアウト幅を設定しています。画面全体を広く使うレイアウトに指定しています。LINE 130: ページタイトルの表示。
画面の見出しとして「経費分析ダッシュボード」というタイトルを表示しています。利用者がどの画面かを一目で分かるようにしています。LINE 131: 操作案内文の表示。
CSVアップロードから絞り込み分析までの流れを簡単に説明する案内文を表示しています。初めて使う利用者への補足説明です。LINE 134: ファイルアップローダーの生成開始。
CSVファイルをアップロードするためのウィジェットを作成する処理を開始しています。以降の引数で表示内容や制約を指定しています。LINE 135: アップロード欄の説明文。
アップロード欄に表示する説明文として、必要な列名を含めた案内文を指定しています。利用者が正しい形式のファイルを準備しやすくしています。LINE 136: アップロード可能な拡張子の指定。
アップロードできるファイルの種類をCSVに限定する設定をしています。誤った形式のファイルを防ぐための指定です。LINE 137: ウィジェットの識別キー指定。
このアップローダーを画面内で識別するためのキーを指定しています。Streamlitが状態を管理する際に使われます。LINE 138: ファイルアップローダー生成の終端。
file_uploaderへの引数指定が終わり、アップロードされたファイルオブジェクトが変数に格納される行です。LINE 140: 未アップロード時の分岐。ファイルがまだアップロードされていない場合に処理を分ける条件文です。
この条件が真のとき、案内メッセージを表示して処理を止めます。LINE 141: 案内メッセージ表示の開始。ファイル未アップロード時に表示するメッセージをHTMLとして組み立てる処理を開始しています。
LINE 142: アップロード案内文の内容。「CSV経費データをアップロードしてください」という文言を画面に表示するdiv要素を指定しています。LINE 143: HTML表示の許可指定。
markdownで生成した内容をHTMLとしてそのまま表示するための設定をしています。この指定によりdiv要素が正しく反映されます。LINE 144: メッセージ表示処理の終端。
st.markdownへの引数指定が終わり、案内メッセージが画面に表示される行です。LINE 145: 処理の停止。ファイルが未アップロードの場合、これ以上の処理を進めないようにst.stopで処理を止めています。
エラーを防ぐための安全な停止処理です。LINE 147: 例外処理の開始。CSVの読み込みや前処理で発生しうるエラーを捕まえるため、try文で処理を囲んでいます。
LINE 148: CSVの読み込みと前処理の実行。アップロードされたファイルを読み込み、日付や金額の型変換を行う前処理をまとめて実行し、結果をexpenses変数に格納しています。LINE 149: 例外の捕捉条件。
値のエラーやCSVの解析エラー、データが空の場合の例外をまとめて捕まえる条件を指定しています。エラー内容はerror変数に格納されます。LINE 150: エラーメッセージ表示の開始。
読み込みに失敗した場合に表示するエラーメッセージをHTMLとして組み立てる処理を開始しています。LINE 151: エラー内容を含む表示文。発生したエラーの内容を含めて「読み込みに失敗しました」というメッセージを表示するdiv要素を指定しています。
LINE 152: HTML表示の許可指定。markdownで生成したエラーメッセージをHTMLとしてそのまま表示するための設定をしています。LINE 153: エラーメッセージ表示処理の終端。
st.markdownへの引数指定が終わり、エラーメッセージが画面に表示される行です。LINE 154: 処理の停止。読み込みエラーが発生した場合、これ以上の処理を進めないようにst.stopで処理を止めています。
LINE 156: 明細が空かどうかの判定。前処理後のデータが空になっていないかを確認する条件文です。日付や金額が不正で全件除外された場合を検知します。
LINE 157: 明細なしメッセージ表示の開始。集計できる明細がない場合に表示するメッセージをHTMLとして組み立てる処理を開始しています。LINE 158: 明細なし時の案内文。
「集計できる明細がありません」という文言と、日付や金額を確認するよう促すメッセージを表示するdiv要素を指定しています。LINE 159: HTML表示の許可指定。markdownで生成した内容をHTMLとしてそのまま表示するための設定をしています。
LINE 160: メッセージ表示処理の終端。st.markdownへの引数指定が終わり、明細なしのメッセージが画面に表示される行です。LINE 161: 処理の停止。
集計可能な明細がない場合、これ以上の処理を進めないようにst.stopで処理を止めています。LINE 164: 月の選択肢を作成。前処理済みデータから月の値を重複なく取り出し、並び替えて選択肢のリストを作成しています。
フィルター用のプルダウンで使われます。LINE 165: 部門の選択肢を作成。前処理済みデータから部門名を重複なく取り出し、並び替えて選択肢のリストを作成しています。
フィルター用のプルダウンで使われます。LINE 166: カテゴリの選択肢を作成。前処理済みデータからカテゴリ名を重複なく取り出し、並び替えて選択肢のリストを作成しています。
フィルター用のプルダウンで使われます。LINE 168: フィルター用の3列レイアウトを作成。st.columns(3)を呼び出して画面を3つの列に分割し、month_column・department_column・category_columnという変数にそれぞれの列を格納しています。
これにより月別・部門別・カテゴリ別のフィルターを横並びに配置できます。LINE 169: 月別フィルター列の開始。withブロックを使ってmonth_columnの中に描画対象を限定しています。
この中に書かれた部品は左側の列に表示されます。LINE 170: 月別フィルターの選択ボックスを配置。st.multiselectを呼び出して、月を複数選択できるフィルターを表示しています。
ユーザーが分析したい月を自由に選べるようになります。LINE 171: 月フィルターの選択肢と初期値を指定。選択肢としてmonth_optionsを渡し、初期状態では全ての月が選ばれているようにdefaultにも同じリストを設定しています。
keyには一意な識別子を指定してウィジェットを区別しています。LINE 172: 月フィルターの呼び出しを閉じる。multiselect関数呼び出しの括弧を閉じ、戻り値がselected_monthsに代入される準備を完了させています。
LINE 173: 部門別フィルター列の開始。withブロックでdepartment_columnの中に描画対象を限定しています。ここに書かれた部品は中央の列に表示されます。
LINE 174: 部門別フィルターの選択ボックスを配置。st.multiselectを呼び出して、部門を複数選択できるフィルターを表示しています。ユーザーが確認したい部門を絞り込めるようになります。
LINE 175: 部門フィルターの選択肢と初期値を指定。選択肢としてdepartment_optionsを渡し、初期状態で全部門が選択された状態にするためdefaultにも同じリストを設定しています。keyでウィジェットを一意に識別しています。
LINE 176: 部門フィルターの呼び出しを閉じる。multiselect関数呼び出しの括弧を閉じ、戻り値がselected_departmentsに代入される準備を完了させています。LINE 177: カテゴリ別フィルター列の開始。
withブロックでcategory_columnの中に描画対象を限定しています。ここに書かれた部品は右側の列に表示されます。LINE 178: カテゴリ別フィルターの選択ボックスを配置。
st.multiselectを呼び出して、カテゴリを複数選択できるフィルターを表示しています。ユーザーが見たいカテゴリだけに絞り込めるようになります。LINE 179: カテゴリフィルターの選択肢と初期値を指定。
選択肢としてcategory_optionsを渡し、初期状態で全カテゴリが選択された状態にするためdefaultにも同じリストを設定しています。keyでウィジェットを一意に識別しています。LINE 180: カテゴリフィルターの呼び出しを閉じる。
multiselect関数呼び出しの括弧を閉じ、戻り値がselected_categoriesに代入される準備を完了させています。LINE 182: 分析更新ボタンの判定。st.buttonで「分析を更新」ボタンを表示し、クリックされた場合にTrueを返すのでその条件でif文の中の処理を実行します。
type="primary"により目立つ色のボタンとして表示されます。LINE 183: 分析実行フラグの保存。ボタンが押されたことをst.session_stateの"analyzed"というキーにTrueとして保存しています。
これにより再描画後もボタンが押された状態を覚えておけます。LINE 185: 未分析状態の判定。session_stateから"analyzed"の値を取得し、まだ分析が実行されていない場合にTrueとなる条件をチェックしています。
デフォルト値としてFalseを指定しているため初回表示時にも安全に動作します。LINE 186: 未分析メッセージ表示の開始。st.markdownを呼び出して、まだ分析が実行されていないことをユーザーに伝えるHTMLメッセージの描画を開始しています。
LINE 187: 未分析メッセージの本文。「フィルターを選んで『分析を更新』を押してください」という案内文をdiv要素として渡しています。IDにdashboard-statusを指定して画面上の位置を特定できるようにしています。
LINE 188: HTML表示の許可設定。unsafe_allow_htmlをTrueに指定して、文字列内のHTMLタグをそのままブラウザに描画できるようにしています。LINE 189: 未分析メッセージ呼び出しの終了。
st.markdown関数呼び出しの括弧を閉じ、メッセージの表示処理を完了させています。LINE 190: 未分析時の処理停止。st.stop()を呼び出して、これ以降のコードの実行を止めています。
分析ボタンが押されるまで下部のグラフなどは表示されません。LINE 192: フィルター条件で明細を絞り込み。apply_filters関数にexpensesと選択済みの月・部門・カテゴリを渡し、条件に合う明細だけを抽出した結果をfilteredに代入しています。
この結果がこれ以降の集計やグラフの元データになります。LINE 194: 絞り込み結果が空かどうかの判定。filteredデータフレームが空かどうかをチェックしています。
空であればこの後のメッセージ表示と処理停止に進みます。LINE 195: 該当データなしメッセージ表示の開始。st.markdownを呼び出して、選択条件に一致する明細が無いことを伝えるHTMLメッセージの描画を開始しています。
LINE 196: 該当データなしメッセージの本文。「選択した条件に一致する明細がありません」という案内文をdiv要素として渡しています。IDにdashboard-statusを指定して表示位置を統一しています。
LINE 197: HTML表示の許可設定。unsafe_allow_htmlをTrueに指定して、メッセージ内のHTMLタグがそのまま画面に反映されるようにしています。LINE 198: 該当データなしメッセージ呼び出しの終了。
st.markdown関数呼び出しの括弧を閉じ、メッセージの表示処理を完了させています。LINE 199: 該当データなし時の処理停止。st.stop()を呼び出して、絞り込み結果が空のときにグラフなど以降の処理が実行されないようにしています。
LINE 202: KPI集計の実行。build_kpi関数にfilteredを渡して、合計金額や件数、平均、最大の月・部門・カテゴリなどの集計結果を辞書としてkpiに代入しています。この結果が次に表示するカードの値として使われます。
LINE 203: KPIカード表示の開始。st.markdownを呼び出して、複数行にわたるHTML文字列でKPIカードを描画する処理を開始しています。LINE 204: 複数行f文字列の開始。
三重引用符によるf文字列を開始し、複数のdiv要素を含むHTMLをまとめて記述しています。LINE 205: KPIカードの外枠を定義。kpi-summaryというIDを持つdivを用意し、flexレイアウトで複数のカードを横並びに表示できるようスタイルを指定しています。
LINE 206: 合計経費カードの表示。kpi['total']をカンマ区切りの数値としてフォーマットし、「合計経費」というラベルとともにカード形式で表示しています。CARD_STYLEによって見た目が統一されています。
LINE 207: 明細件数カードの表示。kpi['count']の値を使って「明細件数」というラベルとともに絞り込み後の件数をカード形式で表示しています。LINE 208: 平均経費カードの表示。
kpi['average']をカンマ区切りの数値としてフォーマットし、「1件あたり平均」というラベルとともにカード形式で表示しています。LINE 209: 最大部門カードの表示。kpi['top_department']の値を使って、経費が最も多かった部門名を「最大部門」というラベルとともにカード形式で表示しています。
LINE 210: KPIカード外枠の終了タグ。kpi-summaryのdivを閉じるタグで、これまでの4つのカードをひとつのフレックスコンテナとしてまとめています。LINE 211: 複数行f文字列の終了。
三重引用符を閉じて、KPIカード用のHTML文字列全体を完成させています。LINE 212: HTML表示の許可設定。unsafe_allow_htmlをTrueに指定して、KPIカードのHTMLがそのままブラウザに反映されるようにしています。
LINE 213: KPIカード表示呼び出しの終了。st.markdown関数呼び出しの括弧を閉じ、KPIカードの描画処理を完了させています。LINE 216: 月別推移セクション見出しの表示。
st.markdownを使って「月別推移」という見出しをsection-monthというIDのdivとして表示しています。SECTION_STYLEにより見出しのデザインが統一されています。LINE 217: 月別経費グラフの表示。
build_monthly_figure関数でfilteredから作成した折れ線グラフをst.plotly_chartで画面に描画しています。use_container_width=Trueにより画面幅に合わせて表示されます。LINE 218: 月別サマリー表示の開始。
st.markdownを呼び出して、月別グラフの下に表示する要約テキストの描画を開始しています。LINE 219: 月別サマリーの開始タグ。month-summaryというIDを持つdivを開始し、SUMMARY_STYLEを適用した要約テキストの枠を用意しています。
LINE 220: 最大月の情報を埋め込み。kpiに含まれる最大月の名称と金額をカンマ区切りでフォーマットし、「最大月」という文言とともにテキストを完成させています。LINE 221: HTML表示の許可設定。
unsafe_allow_htmlをTrueに指定して、要約テキストのHTMLがそのまま画面に反映されるようにしています。LINE 222: 月別サマリー表示呼び出しの終了。st.markdown関数呼び出しの括弧を閉じ、月別サマリーの描画処理を完了させています。
LINE 224: 部門別とカテゴリ別の2列レイアウトを作成。st.columns(2)を呼び出して画面を左右2列に分割し、left_columnとright_columnという変数にそれぞれの列を格納しています。これにより部門別グラフとカテゴリ別グラフを並べて表示できます。
LINE 225: 部門別表示列の開始。withブロックを使ってleft_columnの中に描画対象を限定しています。この中に書かれた部品は左側の列に表示されます。
LINE 227: 部門別比較セクション見出しの表示。st.markdownを呼び出して、「部門別比較」という見出しをsection-departmentというIDのdivとして表示する処理を開始しています。LINE 228: 部門別比較見出しの本文。
SECTION_STYLEを適用した見出しテキストとして「部門別比較」という文字列を渡しています。LINE 229: HTML表示の許可設定。unsafe_allow_htmlをTrueに指定して、見出しのHTMLがそのままブラウザに反映されるようにしています。
LINE 230: 部門別比較見出し呼び出しの終了。st.markdown関数呼び出しの括弧を閉じ、見出しの描画処理を完了させています。LINE 231: 部門別経費グラフの表示。
build_department_figure関数でfilteredから作成した横棒グラフをst.plotly_chartで画面に描画しています。use_container_width=Trueにより列の幅に合わせて表示されます。LINE 232: 部門別サマリー表示の開始。
st.markdownを呼び出して、部門別グラフの下に表示する要約テキストの描画を開始しています。LINE 233: 部門別サマリーの開始タグ。department-summaryというIDを持つdivを開始し、SUMMARY_STYLEを適用した要約テキストの枠を用意しています。
LINE 234: 最大部門の情報を埋め込み。kpiに含まれる経費が最も多い部門の名称と金額をカンマ区切りでフォーマットし、「最大部門」という文言とともにテキストを完成させています。LINE 235: HTML表示の許可設定。
unsafe_allow_htmlをTrueに指定して、要約テキストのHTMLがそのまま画面に反映されるようにしています。LINE 236: 部門別サマリー表示呼び出しの終了。st.markdown関数呼び出しの括弧を閉じ、部門別サマリーの描画処理を完了させています。
LINE 237: カテゴリ別表示列の開始。withブロックを使ってright_columnの中に描画対象を限定しています。この中に書かれた部品は右側の列に表示されます。
LINE 239: カテゴリ構成セクション見出しの表示。st.markdownを呼び出して、「カテゴリ構成」という見出しをsection-categoryというIDのdivとして表示する処理を開始しています。LINE 240: カテゴリ構成見出しの本文。
SECTION_STYLEを適用した見出しテキストとして「カテゴリ構成」という文字列を渡しています。LINE 241: HTML表示の許可設定。unsafe_allow_htmlをTrueに指定して、見出しのHTMLがそのままブラウザに反映されるようにしています。
LINE 242: カテゴリ構成見出し呼び出しの終了。st.markdown関数呼び出しの括弧を閉じ、見出しの描画処理を完了させています。LINE 243: カテゴリ構成グラフの表示。
絞り込んだ明細からbuild_category_figureで作成したドーナツグラフを画面に表示します。use_container_widthをTrueにすることで、右側の列の幅に合わせてグラフが自動的に広がります。LINE 244: カテゴリ要約表示の開始。
st.markdownの呼び出しを開始し、カテゴリごとの要約テキストをHTMLとして描画する準備をします。ここから複数行にわたって表示内容を組み立てていきます。LINE 245: 要約用のdiv要素を定義。
id属性にcategory-summaryを指定したdiv要素を作成し、SUMMARY_STYLEで文字色や余白などの見た目を整えています。この枠の中にカテゴリの集計結果が表示されます。LINE 246: 最大カテゴリの金額を埋め込み。
kpi辞書からtop_categoryとtop_category_amountを取り出し、最も経費が大きいカテゴリ名とその金額をカンマ区切りの円表示で文章に埋め込みます。LINE 247: HTMLタグの表示を許可。unsafe_allow_htmlをTrueに設定し、これまで組み立てたdivタグを含む文字列をそのままHTMLとして画面に反映させます。
LINE 248: markdown呼び出しの終了。st.markdownの呼び出しを閉じ、右側の列に配置されたカテゴリ構成セクションの表示処理を完了させます。PREVIEW: 完成した経費分析ダッシュボードを操作。
入力・送信・結果表示を実際のブラウザ画面で確認します。UPLOAD: 実際の入力データでアプリを操作。CSV経費データをアップロードして分析を更新すると、合計経費のKPIカードと月別推移サマリーが表示されました 確認1/5: CSV投入後の合計経費を見る。
宣言済みの経費CSVを実際に投入し、分析更新後の合計経費と明細件数をKPIカードで確認します。確認2/5: 1件あたりの平均を確かめる。同じ経費CSVの全明細を対象に、明細件数と1件あたり平均をKPIカードで確認します。
確認3/5: 月別経費のピークを確認する。月別経費の折れ線グラフと要約を表示し、経費が最大の月と金額を確認します。確認4/5: 部門別経費を比較する。
部門別経費の横棒グラフと要約を表示し、金額が最大の部門を確認します。確認5/5: カテゴリ別の構成を見る。カテゴリ構成のドーナツグラフと要約を表示し、金額が最大のカテゴリを確認します。
学習内容のまとめ。st.file_uploaderでCSV経費データを受け取るpx.lineとmarkersで月別推移を描くutf-8-sigとcp932を順に試して読み込む 読み込み失敗時にエラー内容を画面へ出す 小さく実行確認しながら完成状態まで段階的に組み立てる エンディング。Python研修はCodeCampでご確認ください。
PythonのStreamlitとPlotlyとは
今回使用する主要なライブラリについて、役割と使い分けを順番に確認します。
だけで画面を作れるStreamlit
StreamlitはPythonのスクリプトを上から実行するだけでブラウザ画面を組み立てられるWebアプリフレームワークです。HTMLやJavaScriptを書かなくても、見出し、ファイルアップローダー、選択ボックス、ボタンといった部品を関数呼び出しで並べられます。
操作されるたびにスクリプト全体が再実行される仕組みのため、残したい情報だけをst.session_stateへ預ける設計になりました。今回の経費分析ダッシュボードでも、CSVが未選択のうちはst.stopで以降の描画を止め、入力がそろってから集計へ進みます。
経費分析ダッシュボードで実際に使ったStreamlitの部品と、その役割を挙げます。
- st.file_uploaderでCSV経費データを受け取る
- st.multiselectで月・部門・カテゴリを選ばせる
- st.columnsでフィルターとグラフを横に並べる
- st.buttonの押下結果をst.session_stateへ保存する
- st.stopで条件が整うまで描画を打ち切る
- st.markdownのunsafe_allow_htmlでKPIカードを装飾する
グラフ表示を担うPlotly Express
Plotly ExpressはDataFrameを渡すだけで拡大やホバーに対応した図を描けるグラフ描画ライブラリです。折れ線はpx.line、横棒はpx.bar、ドーナツはpx.pieというように、グラフの種類ごとに関数が用意されています。
作られたfigureはupdate_tracesやupdate_layoutで後から調整でき、軸名やホバーの書式も差し替えられました。Streamlit側はst.plotly_chartへ渡すだけで画面に埋め込め、幅もコンテナに合わせられます。
経費分析ダッシュボードで描いた3種類のグラフと、加えた調整を挙げます。
- px.lineとmarkersで月別推移を描く
- px.barのorientationで部門別を横棒にする
- px.pieのholeでカテゴリ構成をドーナツにする
- update_layoutで軸名と高さを指定する
- update_tracesでホバーを桁区切りの円表示にする
- st.plotly_chartのuse_container_widthで幅を合わせる
Python・Streamlitで開発する場合の環境構築
この記事のセットアップ手順と掲載コードは、Windows 11 Pro、PowerShell 5.1、Python 3.13.3で動作確認しています。仮想環境を有効化せず、その中のPythonを直接指定するため、以下のコマンドはPowerShellとコマンドプロンプト(cmd)の両方で使えます。
python -m venv .venv
.\.venv\Scripts\python.exe -m pip install --upgrade pip
.\.venv\Scripts\python.exe -m pip install streamlit plotly pandas
macOS・Linuxでは仮想環境内のPythonパスが異なります。今回の動作確認環境とは異なるため、以下は環境差分を補う参考手順です。
python3 -m venv .venv
./.venv/bin/python -m pip install --upgrade pip
./.venv/bin/python -m pip install streamlit plotly pandas
- 起動は .\.venv\Scripts\python.exe -m streamlit run expense_dashboard.pyです。既定でブラウザが開き、閉じてしまった場合はターミナルに表示されるLocal URLを開き直します。
- 読み込むCSVはdate, department, category, amountの4列が必須です。列名が違う場合は画面にエラーメッセージが出ます。
- グラフはブラウザ描画のみなので、Plotlyの画像書き出し用パッケージ(kaleido)は入れなくても動きます。
経費分析ダッシュボードの要件定義
目的は、PythonのStreamlitとPlotlyでCSV経費データを読み込み、月別・部門別・カテゴリ別に絞り込んだ合計経費や最大部門をブラウザ画面で確認できるダッシュボードを作ることです。
対象者として、Pythonの基本文法とpandasの基礎に触れたことがあり、表データをブラウザ上のグラフとして見せる方法を学びたい人を想定しています。
完成物は、CSV経費データをアップロードすると合計経費のKPIカードと月別推移・部門別比較・カテゴリ構成のグラフを表示する、StreamlitとPlotly製の経費分析ダッシュボードです。
実装へ入る前に、機能・品質・受け入れ条件を分けて確認します。
機能要件
- st.file_uploaderでCSV経費データを受け取る
- utf-8-sigとcp932を順に試して読み込む
- date・department・category・amountの列の有無を検証する
- 日付と金額を型変換して欠損行を除外する
- date列からYYYY-MM形式の月列を作る
- 月別・部門別・カテゴリ別のマルチセレクトで絞り込む
- 分析を更新ボタンの押下で集計へ進む
- 合計経費と明細件数と平均と最大部門をKPIカードに出す
- 月別合計を折れ線グラフで描く
- 部門別合計を横棒グラフで描く
- カテゴリ別合計をドーナツグラフで描く
- 各グラフの下へ最大の月・部門・カテゴリを添える
非機能要件
- 読み込み失敗時にエラー内容を画面へ出す
- 未選択の軸は絞り込み条件から外す
- 集計できる明細がない場合はst.stopで処理を止める
- st.set_page_configでワイドレイアウトを指定する
- KPIカードと見出しの装飾を定数へ集約する
- 読み込みと集計と可視化を関数へ分ける
- 分析の実行状態をst.session_stateへ保持する
- 折れ線グラフのホバー表示を桁区切りの円単位に整える
実装方針
今回はStreamlitとPlotlyの基本動作を追いやすくするため、経費分析ダッシュボード本体を1つのPythonファイルへまとめます。
入力、判定、結果表示の役割を分け、実行結果を確認しながら機能を積み上げます。
経費分析ダッシュボードを安全に組み立てるための実装方針は次のとおりです。
- 読み込み失敗時にエラー内容を画面へ出す
- 未選択の軸は絞り込み条件から外す
- 集計できる明細がない場合はst.stopで処理を止める
- st.set_page_configでワイドレイアウトを指定する
- KPIカードと見出しの装飾を定数へ集約する
- 読み込みと集計と可視化を関数へ分ける
- 分析の実行状態をst.session_stateへ保持する
- 折れ線グラフのホバー表示を桁区切りの円単位に整える
完成と判断する条件
- CSV未選択時にアップロードを促す案内が出る
- 必要列が欠けたCSVで不足列名付きのエラーが出る
- 分析を更新を押すまで集計結果が出ない
- 更新後に合計経費と明細件数のKPIカードが並ぶ
- 月別推移の下に最大月と金額が出る
- 部門別比較とカテゴリ構成が左右に並ぶ
- 条件に一致しない場合に該当なしの案内が出る
StreamlitとPlotlyで経費ダッシュボードを作る際の重要ポイント
ダッシュボードの中身は、読み込み、前処理、絞り込み、集計という4つの関数に分かれています。読み込みでは文字コードを順に試し、必要な4列がそろっているかを確かめてから次へ進みます。前処理は日付と金額を型変換して欠損行を落とし、集計の軸になるYYYY-MM形式の月列を加える役割です。
最後に選ばれた条件で明細を絞り込み、合計や件数、最大の月・部門・カテゴリを辞書へまとめました。
読み込みから集計までで押さえておきたい処理の要点を挙げます。
- utf-8-sigとcp932を順に試して読み込む
- 不足した列名をそのままエラーへ含める
- coerce指定で変換できない値を欠損にする
- dropnaで日付と金額が欠けた明細を除く
- strftimeでYYYY-MM形式の月列を作る
- 未選択のフィルターは絞り込みに使わない
- idxmaxで最大の月・部門・カテゴリを取り出す
文字コードと列の検証で読み込みを守る
日本語のCSVはUTF-8とCP932が混在しがちです。read_expense_csvではまずutf-8-sigで読み、UnicodeDecodeErrorが出たらcp932で読み直し、どちらも失敗したときは保存形式を促すエラーを投げます。列の検証も同じ関数が担い、足りない列名をメッセージへ並べるため、利用者は何を直せばよいか画面上で判断できました。
読み込み関数が担当している検査を順に示します。
- バイト列をio.BytesIOで包んで読み直す
- UnicodeDecodeErrorを捕まえて次の文字コードへ進む
- 判定できない場合はValueErrorを送出する
- 不足列名をカンマ区切りでメッセージへ入れる
型変換と月列の作成で集計しやすくする
文字列のまま扱うと、日付の並べ替えも金額の合計もうまくいきません。preprocessではto_datetimeとto_numericで型をそろえ、変換できなかった行はdropnaで取り除きます。さらに部門名とカテゴリ名の前後の空白を落とし、表記のゆれで別項目として数えられる事故を防ぎました。
仕上げに日付順へ並べ替え、月列を加えた状態で返します。
前処理が行う変換をひとつずつ挙げます。
- to_datetimeで日付列を日付型にする
- to_numericで金額列を数値型にする
- stripで部門名とカテゴリ名の空白を落とす
- sort_valuesで日付順に並べ替える
- reset_indexで連番を振り直す
絞り込みとKPI集計で画面の数値を作る
apply_filtersは月・部門・カテゴリを順に見て、選択があるときだけisinで絞り込みます。すべて未選択なら元の明細をそのまま返すため、初期表示でも数値が消えません。build_kpiは絞り込み後のデータを3つの軸でgroupbyし、合計と件数から平均を求め、idxmaxとmaxで最大の軸名と金額を辞書へまとめました。
画面側はこの辞書を読むだけでKPIカードと要約文を描けます。
絞り込みと集計が返す値の中身を具体的に示します。
- isinで選択された値だけを残す
- 未選択の軸は絞り込みを飛ばす
- month・department・categoryごとに合計する
- 合計を件数で割って平均を出す
- idxmaxで最大の軸名を取得する
PythonでCSV経費データを整える前処理設計
CSVは人の手で作られるので、日付の書式や金額のカンマが行ごとにばらつきます。そのまま集計へ回すと型が混ざるため、整える工程を1つの関数へまとめました。
文字コードはUTF-8とCP932の2通りを順に試す方針です。Excelから書き出したCSVはCP932になりやすく、片方だけを想定すると読み込みでつまずきます。
月別集計のために、日付からYYYY-MM形式のmonth列を先に作っておきます。文字列で持たせておくと、並び順もグラフの軸も素直に扱えました。
ポイントとしては、前処理で必ず押さえたい3つの変換ルールをまとめます。
このセクションの用語
- DataFrame
-
pandasが持つ表形式のデータ構造です。CSVの行と列をそのまま扱え、列単位の計算や絞り込みができます。 - 欠損値(NaN)
- 値が入っていない、または数値へ変換できなかったことを示す印です。集計では行ごと除外することが多くなります。
- CP932
- Windowsの日本語環境で広く使われる文字コードです。ExcelからCSVを保存するとこの形式になることがあります。
- 前処理
- 分析の前にデータの型や表記をそろえる作業です。今回は型変換・空白除去・月列の追加が該当します。
| 列 | 役割 | 前処理での扱い |
|---|---|---|
| date | 経費が発生した日付 |
pd.to_datetimeで日付型に変換し、変換できない行は除外 |
| department | 部門名 | 文字列化したうえでstr.stripで前後の空白を除去 |
| category | 費目やカテゴリ名 | 文字列化と空白除去で表記のゆれを吸収 |
| amount | 金額 |
pd.to_numericで数値化し、数値にならない行は除外 |
| month | 集計用に生成する列 |
dateからYYYY-MM形式の文字列を作成 |
日付:to_datetimeで統一
金額:文字列混在はNaN化
月列:YYYY-MM形式で集計
Pythonで経費分析ダッシュボードの完成コード
コードは1ファイル構成で、読み込み・前処理・絞り込み・集計をそれぞれ関数に分けました。入口も出口もDataFrameなので、途中の処理だけを単体で確かめられます。
主役のライブラリはpandas・plotly.express・streamlitの3つです。pxには集計済みの結果を渡すだけにして、計算を持ち込まない形にしています。
KPIカードや見出しの見た目はCARD_STYLEなどの定数に寄せました。レイアウトを直したいときに触る場所が1か所に決まります。
グラフ部分は、部門別の描画直前に「# 部門別経費の可視化」というコメントを独立して置き、カテゴリ構成側のコメントと分けてあります。処理そのものは変えずに、どこで何を描いているかを追いやすくしました。
このセクションの用語
- groupby
- 指定した列の値ごとに行をまとめる
pandasの機能です。まとめたあとにsum()などを続けて集計します。 - isin
- 列の値が渡したリストに含まれる行だけを選ぶ判定です。複数選択のフィルターと相性のよい書き方になっています。
- BytesIO
- バイト列をファイルのように扱えるようにする標準ライブラリの仕組みです。同じ中身を別の文字コードで読み直せます。
- errors="coerce"
- 型変換に失敗した値をエラーにせずNaNへ置き換える指定です。1行の異常で処理全体が止まるのを防げます。
"""PlotlyとStreamlitで作る経費分析ダッシュボード。
起動コマンド: streamlit run expense_dashboard.py
"""
import io
import pandas as pd
import plotly.express as px
import streamlit as st
# 動作を確認したい画面
ROUTES = ["/"]
REQUIRED_COLUMNS = ["date", "department", "category", "amount"]
CARD_STYLE = (
"flex:1 1 200px;padding:12px 16px;border:1px solid #dfe3e8;"
"border-radius:8px;background:#f7f9fc;font-weight:600;"
)
SECTION_STYLE = "margin:12px 0 4px;font-size:18px;font-weight:700;"
SUMMARY_STYLE = "margin:4px 0 16px;color:#31333f;"
# CSV経費データの読み込み
def read_expense_csv(uploaded_file) -> pd.DataFrame:
"""アップロードされたCSVを文字コードを判定しながら読み込む。"""
raw = uploaded_file.getvalue()
frame = None
for encoding in ("utf-8-sig", "cp932"):
try:
frame = pd.read_csv(io.BytesIO(raw), encoding=encoding)
break
except UnicodeDecodeError:
frame = None
if frame is None:
raise ValueError("文字コードを判定できません(UTF-8またはCP932で保存してください)")
missing = [column for column in REQUIRED_COLUMNS if column not in frame.columns]
if missing:
raise ValueError("必要な列がありません: " + ", ".join(missing))
return frame
# 前処理(型変換と月列の作成)
def preprocess(frame: pd.DataFrame) -> pd.DataFrame:
"""日付と金額を型変換し、集計用の月列(YYYY-MM)を追加する。"""
tidy = frame.loc[:, REQUIRED_COLUMNS].copy()
tidy["date"] = pd.to_datetime(tidy["date"], errors="coerce")
tidy["amount"] = pd.to_numeric(tidy["amount"], errors="coerce")
tidy["department"] = tidy["department"].astype(str).str.strip()
tidy["category"] = tidy["category"].astype(str).str.strip()
tidy = tidy.dropna(subset=["date", "amount"])
tidy["month"] = tidy["date"].dt.strftime("%Y-%m")
return tidy.sort_values("date").reset_index(drop=True)
# 月別・部門別・カテゴリ別フィルター
def apply_filters(frame: pd.DataFrame, months, departments, categories) -> pd.DataFrame:
"""選択された月・部門・カテゴリで明細を絞り込む(未選択の軸は絞り込まない)。"""
filtered = frame
if months:
filtered = filtered[filtered["month"].isin(months)]
if departments:
filtered = filtered[filtered["department"].isin(departments)]
if categories:
filtered = filtered[filtered["category"].isin(categories)]
return filtered.reset_index(drop=True)
# KPIの集計
def build_kpi(frame: pd.DataFrame) -> dict:
"""絞り込み後の明細から合計・件数・平均・最大の軸を集計する。"""
by_month = frame.groupby("month")["amount"].sum()
by_department = frame.groupby("department")["amount"].sum()
by_category = frame.groupby("category")["amount"].sum()
total = float(frame["amount"].sum())
return {
"total": total,
"count": int(len(frame)),
"average": total / len(frame),
"top_month": str(by_month.idxmax()),
"top_month_amount": float(by_month.max()),
"top_department": str(by_department.idxmax()),
"top_department_amount": float(by_department.max()),
"top_category": str(by_category.idxmax()),
"top_category_amount": float(by_category.max()),
}
# 可視化(月別経費の推移)
def build_monthly_figure(frame: pd.DataFrame):
"""月ごとの合計を折れ線グラフにする。"""
monthly = frame.groupby("month", as_index=False)["amount"].sum()
figure = px.line(monthly, x="month", y="amount", markers=True, title="月別経費の推移")
figure.update_traces(hovertemplate="%{x}<br>%{y:,.0f}円<extra></extra>")
figure.update_layout(xaxis_title="月", yaxis_title="経費(円)", height=360)
return figure
# 部門別経費の可視化
def build_department_figure(frame: pd.DataFrame):
"""部門ごとの合計を横棒グラフにして部門別経費の可視化を行う。"""
department = (
frame.groupby("department", as_index=False)["amount"].sum().sort_values("amount")
)
figure = px.bar(
department,
x="amount",
y="department",
orientation="h",
text_auto=True,
title="部門別経費の比較",
)
figure.update_layout(xaxis_title="経費(円)", yaxis_title="部門", height=360)
return figure
# 可視化(カテゴリ構成)
def build_category_figure(frame: pd.DataFrame):
"""カテゴリごとの合計をドーナツグラフにする。"""
category = frame.groupby("category", as_index=False)["amount"].sum()
figure = px.pie(category, names="category", values="amount", hole=0.45, title="カテゴリ構成")
figure.update_traces(textinfo="percent+label")
figure.update_layout(height=360)
return figure
# 画面の初期設定
st.set_page_config(page_title="経費分析ダッシュボード", layout="wide")
st.title("経費分析ダッシュボード")
st.caption("CSV経費データをアップロードし、月別・部門別・カテゴリ別に絞り込んで分析します")
# CSV経費データのアップロード
uploaded_file = st.file_uploader(
"CSV経費データ(列: date, department, category, amount)",
type="csv",
key="expense_csv",
)
if uploaded_file is None:
st.markdown(
'<div id="dashboard-status">CSV経費データをアップロードしてください</div>',
unsafe_allow_html=True,
)
st.stop()
try:
expenses = preprocess(read_expense_csv(uploaded_file))
except (ValueError, pd.errors.ParserError, pd.errors.EmptyDataError) as error:
st.markdown(
f'<div id="dashboard-status">読み込みに失敗しました: {error}</div>',
unsafe_allow_html=True,
)
st.stop()
if expenses.empty:
st.markdown(
'<div id="dashboard-status">集計できる明細がありません(日付と金額を確認してください)</div>',
unsafe_allow_html=True,
)
st.stop()
# フィルター操作(月別・部門別・カテゴリ別)
month_options = sorted(expenses["month"].unique().tolist())
department_options = sorted(expenses["department"].unique().tolist())
category_options = sorted(expenses["category"].unique().tolist())
month_column, department_column, category_column = st.columns(3)
with month_column:
selected_months = st.multiselect(
"月別フィルター", month_options, default=month_options, key="month_filter"
)
with department_column:
selected_departments = st.multiselect(
"部門別フィルター", department_options, default=department_options, key="department_filter"
)
with category_column:
selected_categories = st.multiselect(
"カテゴリ別フィルター", category_options, default=category_options, key="category_filter"
)
if st.button("分析を更新", key="run_analysis", type="primary"):
st.session_state["analyzed"] = True
if not st.session_state.get("analyzed", False):
st.markdown(
'<div id="dashboard-status">フィルターを選んで「分析を更新」を押してください</div>',
unsafe_allow_html=True,
)
st.stop()
filtered = apply_filters(expenses, selected_months, selected_departments, selected_categories)
if filtered.empty:
st.markdown(
'<div id="dashboard-status">選択した条件に一致する明細がありません</div>',
unsafe_allow_html=True,
)
st.stop()
# KPIの表示
kpi = build_kpi(filtered)
st.markdown(
f"""
<div id="kpi-summary" style="display:flex;gap:12px;flex-wrap:wrap;">
<div id="kpi-total-amount" style="{CARD_STYLE}">合計経費: {kpi['total']:,.0f}円</div>
<div id="kpi-record-count" style="{CARD_STYLE}">明細件数: {kpi['count']}件</div>
<div id="kpi-average-amount" style="{CARD_STYLE}">1件あたり平均: {kpi['average']:,.0f}円</div>
<div id="kpi-top-department" style="{CARD_STYLE}">最大部門: {kpi['top_department']}</div>
</div>
""",
unsafe_allow_html=True,
)
# 月別経費の可視化
st.markdown(f'<div id="section-month" style="{SECTION_STYLE}">月別推移</div>', unsafe_allow_html=True)
st.plotly_chart(build_monthly_figure(filtered), use_container_width=True)
st.markdown(
f'<div id="month-summary" style="{SUMMARY_STYLE}">'
f'最大月: {kpi["top_month"]} / {kpi["top_month_amount"]:,.0f}円</div>',
unsafe_allow_html=True,
)
left_column, right_column = st.columns(2)
with left_column:
# 部門別経費の可視化
st.markdown(
f'<div id="section-department" style="{SECTION_STYLE}">部門別比較</div>',
unsafe_allow_html=True,
)
st.plotly_chart(build_department_figure(filtered), use_container_width=True)
st.markdown(
f'<div id="department-summary" style="{SUMMARY_STYLE}">'
f'最大部門: {kpi["top_department"]} / {kpi["top_department_amount"]:,.0f}円</div>',
unsafe_allow_html=True,
)
with right_column:
# カテゴリ構成の可視化
st.markdown(
f'<div id="section-category" style="{SECTION_STYLE}">カテゴリ構成</div>',
unsafe_allow_html=True,
)
st.plotly_chart(build_category_figure(filtered), use_container_width=True)
st.markdown(
f'<div id="category-summary" style="{SUMMARY_STYLE}">'
f'最大カテゴリ: {kpi["top_category"]} / {kpi["top_category_amount"]:,.0f}円</div>',
unsafe_allow_html=True,
)
コード全文は上の折り畳みに入れてあるので、全部を上から読む必要はありません。ここでは特に重要な部分だけを抜き出して、何をしているのか順番に見ていきます。
必須列を固定するREQUIRED_COLUMNS
REQUIRED_COLUMNS = ["date", "department", "category", "amount"]扱う列名を最初に定数として決めています。読み込み時のチェックにも前処理の列選択にもこの1行を使い回すので、仕様変更のときに直す場所が1か所で済みます。
read_expense_csvの文字コード判定ループ
raw = uploaded_file.getvalue()
frame = None
for encoding in ("utf-8-sig", "cp932"):
try:
frame = pd.read_csv(io.BytesIO(raw), encoding=encoding)
break
except UnicodeDecodeError:
frame = Noneアップロードされた中身を一度バイト列として受け取り、io.BytesIOで包んでから2つの文字コードを順に試します。UTF-8で失敗しても例外を捕まえてCP932を試すため、ExcelのCSVでも読み込めます。
必須列の欠落をValueErrorで止める
missing = [column for column in REQUIRED_COLUMNS if column not in frame.columns]
if missing:
raise ValueError("必要な列がありません: " + ", ".join(missing))
return frame読み込んだ表に必須列がそろっているかを確認し、足りない列名を集めています。欠けたまま集計へ進むと分かりにくいエラーになるので、ここで足りない列名を添えて止める設計です。
preprocessの型変換と月列づくり
tidy["date"] = pd.to_datetime(tidy["date"], errors="coerce")
tidy["amount"] = pd.to_numeric(tidy["amount"], errors="coerce")
tidy = tidy.dropna(subset=["date", "amount"])
tidy["month"] = tidy["date"].dt.strftime("%Y-%m")日付と金額を型変換し、変換できなかった値はNaNとしてdropnaで落としています。残った日付からYYYY-MMの文字列を作り、月別集計の軸になるmonth列を用意します。
apply_filtersのisinによる3軸絞り込み
filtered = frame
if months:
filtered = filtered[filtered["month"].isin(months)]
if departments:
filtered = filtered[filtered["department"].isin(departments)]
if categories:
filtered = filtered[filtered["category"].isin(categories)]
return filtered.reset_index(drop=True)選択された値のリストをisinに渡し、月・部門・カテゴリの順に行を絞り込みます。リストが空の軸はifが偽になり絞り込みを飛ばすので、未選択は「その軸では絞らない」という意味になります。
build_kpiのgroupbyで軸別に合計
by_month = frame.groupby("month")["amount"].sum()
by_department = frame.groupby("department")["amount"].sum()
by_category = frame.groupby("category")["amount"].sum()
total = float(frame["amount"].sum())絞り込み後の明細を月・部門・カテゴリごとにまとめ、それぞれ金額を合計しています。ここで作った3つの集計結果がKPIカードの材料にも、各グラフの元データにもなります。
PythonのStreamlitとPlotlyのエラー対処
経費CSVは配布元によって書式が違うため、読み込みまわりでつまずくことが多い部分です。ここでは一般に起こりやすいエラーと、切り分けの目安を並べます。
エラー文言そのものより、どの工程で止まったかを見るのが近道です。文字コード・列名・環境の3方向で疑うと、原因にたどり着きやすくなります。
このセクションの用語
- ModuleNotFoundError
-
importしようとしたライブラリが見つからないときに出るエラーです。多くはインストール漏れか環境の違いが原因になっています。 - 仮想環境
- プロジェクトごとにライブラリを分けて入れる仕組みです。別プロジェクトのバージョンと衝突しにくくなります。
- ValueError
- 値の内容が想定と違うときに送出される例外です。今回は必須列が欠けた場合に自分で送出しています。
| エラー例 | 原因 | 対処 |
|---|---|---|
| UnicodeDecodeError | CSVがUTF-8でもCP932でもない文字コードで保存されている | Excelなら「CSV UTF-8」で保存し直すか、読み込み候補の文字コードを追加する |
| ValueError: 必要な列がありません: amount | ヘッダーが日本語のままなど、必須列名と一致していない | ヘッダー行をdate・department・category・amountにそろえるか、読み込み後にrenameで合わせる |
| ModuleNotFoundError: No module named 'plotly' | 起動に使っている環境にライブラリが入っていない | 仮想環境を有効にしてから入れ直し、同じ環境でstreamlit runする |
| streamlit: command not found | 仮想環境が有効化されていない、またはPATHが通っていない | 仮想環境を有効化して実行するか、python -m streamlit runの形で起動する |
| グラフが空でKPIが0になる | フィルターの組み合わせで該当行が0件、または日付や金額がNaNで除外された | 選択を1軸ずつ外して範囲を広げ、元CSVの日付書式と金額の桁区切りを見直す |
経費分析ダッシュボードで注意したい点
Streamlitはウィジェットを操作するたびにスクリプトを先頭から実行し直します。この仕組みを知らないと、「なぜ毎回読み込みからやり直しているように見えるのか」で戸惑いがちです。
公式ドキュメントも、画面を更新する必要があるときはスクリプト全体を上から下へ再実行すると説明しています。処理が重くなるなら、読み込みと前処理を関数として切り出しておくと手当てしやすいです。
フィルターは未選択なら絞り込まない仕様なので、すべて外した状態が全件表示になります。0件になったときは選択条件だけでなく、日付や金額がNaNで落ちた可能性も疑ってみてください。
ポイントとしては、フィルターと再実行まわりで押さえたい挙動をまとめます。
未選択:その軸は絞り込まない
再実行:操作ごとに全体を再走
0件表示:NaN行の脱落も疑う
経費分析ダッシュボードの動作確認
streamlit run expense_dashboard.pyで起動し、ブラウザで開いた操作前の画面をまず記録しました。この時点ではアップロード待ちの状態で、応答はHTTP 200が返っています。
続いてCSV経費データをアップロードして分析を更新すると、合計経費のKPIカードと月別推移サマリーが表示されました。実ブラウザでの操作分も含め、キャプチャは3枚取得しています。
今回の検証で自動取得したデータ件数は0件で、数値そのものを機械的に集めてはいません。画面が意図どおり描かれるかの確認までが範囲です。
このセクションの用語
- HTTPステータスコード
- サーバーからの応答の種類を表す3桁の数値です。200は要求が正常に処理されたことを示します。
- キャプチャ
- 画面の見た目をそのまま画像として保存したものです。表示崩れや要素の有無を後から確認できます。
取得したキャプチャから確認できた点を挙げます。
- 操作前の画面がエラーなく表示され、HTTP 200で応答していること
- CSVアップロード後の更新で、合計経費のKPIカードが画面に出ること
- 同じ更新で月別推移サマリーも表示され、読み込みから集計までがつながっていること



経費分析ダッシュボードが活きる実務の場面
この画面は、月次で同じCSVが出てくる業務ほど効いてきます。毎回グラフを作り直す手間が消え、条件を切り替える操作だけが残ります。
軸は月・部門・カテゴリの3つあるため、会話の流れに合わせて視点を移せるのが特徴です。全体から内訳へ掘り下げるドリルダウンが、その場でできるのが利点です。
このセクションの用語
- 予実管理
- 予算と実績を比べて差を追いかける管理方法です。部門ごとの使いすぎを早めに見つける目的で使われます。
- ドリルダウン
- 全体の数値から部門や費目へと、細かい単位に掘り下げて見ていく分析の進め方です。
| 使える場面 | 具体的な使い方 |
|---|---|
| 月次の経費締めレビュー | 締め後のCSVを読み込み、月フィルターを当月だけに絞って合計と件数を確認し、伸びが目立つ部門をその場で選び直して原因を追う |
| 部門別の予実管理 | 部門フィルターを1部門にして月別推移を眺め、予算ペースより早く伸びている月をカテゴリ構成で費目まで掘り下げる |
| コスト削減テーマの洗い出し | カテゴリ構成で金額の大きい費目を特定し、その費目だけに絞って月別推移を見て、季節性か恒常的な増加かを見分ける |
| 会議資料のたたき台づくり | フィルターを当てた状態の画面をそのまま共有し、質問が出た軸だけ切り替えて説明することで資料の作り直しを減らす |
| 担当引き継ぎ時のデータ共有 | CSVの列仕様を4列に固定して渡し、担当者が替わっても同じ操作で同じ集計を再現できる状態にしておく |
経費分析ダッシュボード開発のまとめ
読み込む関数、整える関数、絞り込む関数、集計する関数と役割を分けたので、画面側のコードは結果を並べるだけになりました。軸を足したくなったときも、触る場所がすぐ分かります。
実際にCSV経費データをアップロードして分析を更新すると、合計経費のKPIカードと月別推移サマリーが表示されました。まずは手元の経費CSVを4列にそろえるところから試すのがおすすめです。
部門別経費の可視化とカテゴリ構成の描画はコメントで区切ってあるため、グラフの種類を差し替える改造も入り口が見つけやすい構成になっています。
さらに詳しく学べるPython研修の詳細はこちら参考にした一次情報
- ^ Streamlit API reference: st.file_uploader. https://docs.streamlit.io/develop/api-reference/widgets/st.file_uploader, (参照26-08-12).
- ^ Streamlit API reference: st.plotly_chart. https://docs.streamlit.io/develop/api-reference/charts/st.plotly_chart, (参照26-08-12).
- ^ Plotly Express in Python. https://plotly.com/python/plotly-express/, (参照26-08-12).
- ^ pandas User Guide: Group by (split-apply-combine). https://pandas.pydata.org/docs/user_guide/groupby.html, (参照26-08-12).
※内容は執筆時点のものです。ライブラリやサイトの仕様は変わる可能性があるため、公式ドキュメントもあわせてご確認ください。
