Streamlitで投資ダッシュボードを作る(SQLite × Plotly・完全ローカル)
Python経験者向けの投資分析シリーズ第13回。第8回・第11回で作った2つのSQLiteと、第10回・第12回が吐いたCSVを、1画面に並べて表示するStreamlitアプリを作ります。新しいグラフは1つも作りません。use_container_width はもう非推奨、キャッシュはDBの更新に気づかない、再実行のたびにスレッドが変わってsqlite3が落ちる——「器」を作るときに踏む地雷を、実行結果と画面つきで並べます。
- 1. この記事のゴールと連載の位置づけ
- 2. Streamlitは画面を作らない — 操作のたびに頭から走り直す
- 3. 2つのDBを読み取り専用で開く — 接続とクエリでキャッシュを分ける
- 4. つまずき実演①:st.cache_data が更新されない(DBに足したのに画面が変わらない)
- 5. つまずき実演②:use_container_width は、もう非推奨
- 6. CSVを受け取る — 第10回・第12回の出力をそのまま食べる
- 7. 第4〜7回の図と表を、1行で画面に貼る
- 8. 動かす — 起動・通信の隔離・そして「公開しない」という選択
- 9. 部品が1画面になった/このダッシュボードで分からないこと
- 10. よくある質問(FAQ)
1. この記事のゴールと連載の位置づけ
本記事は「Python経験者が、自分の投資ポートフォリオを管理・可視化するアプリを作る」連載の第13回です。第12回の最後で予告したとおり、ここから応用フェーズに入ります。第1回から第12回で作ってきた部品を、ブラウザで動く1画面に束ねます。
本記事が作るのは「自分が既に記録したものを、1画面に並べて見るための道具」です。銘柄の評価も、売買の判断も、この画面は一切しません。並んでいるのは自分で入力した記録の合計と件数であって、資産の査定でも成績表でもありません。記事と画面に登場する銘柄名・株数・金額はすべて動作確認用の架空データです。当サイトは投資助言業(金融商品取引業)の登録をしておらず、個別銘柄の推奨や売買タイミングの助言は行いません。
2つのSQLiteを読み取り専用で開き、集計と明細を表にして、第6回のグラフを貼り、CSVを受け取るところまで。完成版は investment_dashboard.py(373行・1ファイル)で、実行結果と画面は2026年9月7日に確認しました。導入は pip install streamlit pandas plotly の1行です(8章の時価取得だけ yfinance も要ります)。作り方だけ急ぐなら、配布コードを開きながら3章→7章→8章、4〜6章はそこで踏む地雷の先回りです。本文のコードは配布コードからの抜粋で、docstring(説明文)を省いた箇所があります。
このアプリはローカルで起動し、クラウドには載せません。扱うのが「どの銘柄を何株持ち、いくら受け取ったか」という個人の記録だからです(理由と待ち受けアドレスの絞り方は8章)。外部との通信はサイドバーの「時価を取得」ボタンを押したときだけで、押さなければ一度も通信しません。
どの回の部品が、画面のどこに刺さるか
| 連載の回 | 作ったもの | 本記事での使い道 |
|---|---|---|
| 第4回 | 損益一覧の DataFrame | st.dataframe() に渡す(7章) |
| 第5回・第7回 | Figure を返す描画関数 | st.pyplot() の差し込み口(7章) |
| 第6回 | build_dashboard() が返す Plotly の Figure | st.plotly_chart() に渡す(7章) |
| 第8回 | dividends.db(配当の入金記録) | 読み取り専用で開いて集計(3章) |
| 第10回・第12回 | 結果CSV | st.file_uploader で受け取る(6章) |
| 第11回 | perks.db(優待の権利確定月) | 読み取り専用で開いて集計(3章) |
この回では新しいグラフを1つも作りません。グラフを作る話は第5〜7回で終わっていて、ここでやるのはすでにある表と図を、操作できる1つの画面に置くことだけです。
2. Streamlitは画面を作らない — 操作のたびに頭から走り直す
最初に理解しておくことが1つだけあります。Streamlitの公式ドキュメントはこう書いています。
Streamlit apps have a unique data flow: any time something must be updated on the screen, Streamlit reruns your entire Python script from top to bottom.(Basic concepts of Streamlit・2026年9月7日確認)
画面の何かを変える必要が出るたび、スクリプトが先頭から最後まで走り直します。きっかけは2つで、「ソースコードを書き換えたとき」と「利用者がウィジェットを操作したとき」(同ページ)。スライダーを1目盛り動かしただけで、import の下から最後の行まで全部が再実行されます。
第8回・第11回のtkinterとは正反対です。tkinterでは root.mainloop() の中でウィジェットが生き続け、押されたコールバックだけが走ります。Streamlitにはウィジェットが常駐する場所がなく、毎回いちから画面を組み直す。この違いが4章以降の罠の全部につながります。
「器」の選び方 — 3つの方式の住み分け
| 見る観点 | Streamlit(第13回) | tkinter(第8回・第11回) | HTMLを書き出す(第9回) |
|---|---|---|---|
| 画面の作り方 | 操作のたびに全部再実行 | ウィジェットが常駐しコールバック | 1回書き出して終わり |
| 入力・書き込み | 本記事では読むだけ | 入力フォームが本業 | できない |
| グラフの貼りやすさ | PlotlyもmatplotlibもDataFrameも1行 | 画像化して貼る手間が要る | Plotlyならそのまま埋め込める(第9回はmatplotlibのPNG) |
| 渡す・持ち歩く | 相手にもPythonとStreamlitが要る | 同上 | HTMLファイル1つで完結 |
| 起動 | streamlit run(サーバーが立つ) | python app.py | ブラウザで開くだけ |
入力して貯めるなら第8回・第11回のtkinter、誰かに渡すなら第9回のHTML書き出し、手元のデータを並べて眺めるなら本記事のStreamlitです。
再実行モデルの副作用:値は毎回消える
頭から走り直すということは、普通のPython変数は毎回リセットされるということです。選んだ年を残すには st.session_state を使いますが、まだ入っていないキーを読むと KeyError なので、初期化を守るのが定型になります。
if "year" not in st.session_state: # 先に入れておく
st.session_state["year"] = "2026"
本アプリはこの定型を書かずに済ませました。ウィジェットに key を付けると、その値が自動的に st.session_state に入るためです。状態は、自分で持たない方法を探してから持つほうが再実行モデルと相性がよくなります。
3. 2つのDBを読み取り専用で開く — 接続とクエリでキャッシュを分ける
このアプリが読むのは、第8回の dividends.db と第11回の perks.db の2つです。どちらにも1行も書き込みません。約束を守る方法はコメントではなく、開き方そのものです。
@st.cache_resource(show_spinner=False)
def connect_ro(db_path: str) -> sqlite3.Connection:
return sqlite3.connect(f"file:{db_path}?mode=ro", uri=True, check_same_thread=False)
file:...?mode=ro は読み取り専用で開くURIモードで、書き込もうとすると例外になります。
>>> con = sqlite3.connect(f"file:{db}?mode=ro", uri=True)
>>> con.execute("INSERT INTO stocks VALUES ('X','Y','Z')")
sqlite3.OperationalError: attempt to write a readonly database
ファイルが無いときも黙って新規作成せず、sqlite3.OperationalError: unable to open database file で止まります(通常の connect() は空のDBを作ってしまいます)。第9回で「第8回のDBには1行も書き込まない」と決めた約束は、これで連載3回目の継承です。
file: のうしろにバックスラッシュ入りのWindowsパスをそのまま書いて動きます。Path.as_uri() もスラッシュ置換もURLエンコードも要りません。スペースを含むフォルダ名・日本語のフォルダ名でも、無変換のまま接続と読み取りに成功し、書き込みだけがブロックされることを2026年9月7日に確認しました(3パターン × 2書式)。
接続とクエリ結果は、別のキャッシュに載せる
2章のとおり、操作のたびにスクリプトが頭から走ります。素直に書くとDBを毎回開き直すので、キャッシュが要ります。Streamlit公式は用途で使い分けを示していて、データベース接続は st.cache_resource、クエリ結果(DataFrame)は st.cache_data です(Caching)。cache_data は戻り値をpickleして複製を返すしくみなので、接続オブジェクトは載せられません。
@st.cache_data(show_spinner=False)
def read_sql(_conn: sqlite3.Connection, db_path: str, sql: str,
params: tuple, stamp: float) -> pd.DataFrame:
return pd.read_sql_query(sql, _conn, params=params)
第1引数が _conn とアンダースコア始まりなのには理由があります。公式ドキュメントは「ハッシュできない引数(データベース接続など)は引数名の先頭にアンダースコアを付ける」と説明していて、付けないと UnhashableParamError です。キーになるのは残りの db_path・sql・params・stamp の4つ。この stamp が4章の主役です。
再実行のたびにスレッドが変わる — check_same_thread=False が要る理由
check_same_thread=False は、なんとなく付ける引数ではありません。Streamlit公式の記述はこうです。
A script thread runs page code — one thread for each script run in a session.(Multithreading in Streamlit・2026年9月7日確認)
1回の再実行につき1つのスレッド。つまり接続をキャッシュして使い回すと、次の操作では別のスレッドから触ることになります。Pythonの sqlite3 は既定でこれを禁止しています。実際に、既定のまま接続をキャッシュした最小アプリを動かし、再実行させたときのログがこれです。
run thread_id=18336 name=ScriptRunner.scriptThread -> OK rows=8
run thread_id=28428 name=ScriptRunner.scriptThread -> sqlite3.ProgrammingError: SQLite objects
created in a thread can only be used in that same thread.
The object was created in thread id 18336 and this is thread id 28428.
最初の1回は動いて、2回目の操作から落ちます。スレッドIDは毎回変わり、3回目・4回目も同じ例外でした。手元で1回動かして満足すると気づけない壊れ方です。
check_same_thread=False は「書かない」とセットで使うPython公式は、この引数を False にすると接続を複数スレッドから触れる一方で、データ破損を避けるために書き込みの直列化が利用者側で必要になる場合があると注意しています(sqlite3 公式ドキュメント)。本アプリは読み取り専用でしか開かないので、この前提を構造的に満たしています。書き込みもするアプリに、この引数だけコピーしないでください。
2つのDBはつなげない — 「束ねる」は「並べる」こと
ダッシュボードなら結合したくなりますが、この2つのDBはJOINできません。第8回の dividends.ticker は '1234.T' のような市場のコード、第11回の perks.code は 'SMPL-01' のような自分で決めた識別子で、体系が違うため対応表が存在しないからです。だから接続は2本持ち、上段に配当、下段に優待を並べました。「束ねる」は「1つの表にする」ことではなく「同じ視界に入れる」こと——つながらないものをつながないのも設計判断で、画面にもその旨を1行書いています。
もうひとつ、第8回のDBは金額を銭(円×100)の整数で持っています。100で割り忘れると桁が2つずれた数字が堂々と並ぶので、SQLの段階で net_sen / 100.0 AS net_yen と割り、Streamlitに渡す前に型と単位を確定させる方針にしました。この「画面に出す前に DataFrame 側で決めておく」という考え方は、7章の列設定と9章まで一本でつながります。
4. つまずき実演①:st.cache_data が更新されない(DBに足したのに画面が変わらない)
ここがこの記事で一番踏みやすい罠です。第8回のツールで配当を1件入力し、ダッシュボードを再実行しても、数字が1円も変わりません。
バグではありません。公式ドキュメントは、キャッシュを使い回すかどうかの判定材料を2つだけ挙げています。「入力パラメータの値」と「関数の中のコード」です(Caching)。同じページには「Objects in the cache become stale, e.g. because you cached old data from a database.」(キャッシュの中身は古くなる。データベースから引いた古いデータをキャッシュしている場合など)ともあります。つまりキャッシュのキーに、DBファイルの中身は入っていません。
対処は3つ。効き方が違う
| やり方 | いつ新しくなるか | 向いている場面 |
|---|---|---|
| ファイルの更新時刻を引数に混ぜる | DBが更新された次の再実行で即座に | 自分のPCのファイルを読む本記事の構成 |
ttl= を付ける | 指定秒数が過ぎたあと(それまでは古い値) | 更新元を見にいけない外部API等 |
st.cache_data.clear() | ボタンを押した瞬間に全部捨てる | 手動の「読み直す」操作 |
公式は「データベースやAPIからデータを引くときは常に ttl を設定すべき」と書いています。ただし ttl は「その秒数までは古い値でよい」と決める仕組みで、実測でも ttl=1 なら1秒未満の再呼び出しは古い値のまま、1.2秒後に新しい値になりました。読むのが手元のファイルなら、もっと直接的な手があります。更新時刻(mtime)を引数に渡す方法です。キャッシュのキーは引数の値なので、ファイルが書き換われば自動的にキーが変わります。
def db_stamp(db_path: str, watch: bool) -> float:
if not watch:
return 0.0 # 固定値=更新しても同じキー=古い結果が返り続ける
try:
return Path(db_path).stat().st_mtime
except OSError:
return 0.0
この stamp を read_sql() に渡すだけです。サイドバーの「DBの更新時刻を見て読み直す」を戻すと、1回の再実行で追いつきます。
14,907円・5件から17,776円・6件へ、追加した1件(2,869円)ぶんだけ動きました。同じコード・同じDBで、違うのはキャッシュのキーだけです。
この罠は例外もログも出しません。画面には何ごともなく数字が出て、その数字が古いだけです。第12回の丸めや float32 と同じ「エラーが出ないまま結果だけ変わる」事故になります。読み込み関数を書いたら、何を変えたら値が変わるのかを1回試すのが確実です。
5. つまずき実演②:use_container_width は、もう非推奨
「streamlit ダッシュボード」で見つかるサンプルは、ほぼ全部が use_container_width=True で書かれています。この引数はすでに非推奨です。streamlit 1.58.0 で実行すると、次の警告が出ます。
Please replace `use_container_width` with `width`.
`use_container_width` will be removed after 2025-12-31.
For `use_container_width=True`, use `width='stretch'`. For `use_container_width=False`, use `width='content'`.
st.dataframe でも st.plotly_chart でも同じ文言が出ました。削除予定日として書かれている 2025-12-31 は、この記事を書いている2026年9月時点で既に過ぎています。1.58.0 ではまだ動きますが、いつ消えても文句は言えない状態です。書き換えは機械的で、True なら width="stretch"、False なら width="content"。
st.dataframe(df, width="stretch", hide_index=True,
column_config=columns, height=height)
厄介なのは、非推奨の警告が画面ではなくターミナルにだけ出ることです。ブラウザ側は普通に表示されるので、streamlit run を実行したターミナルを見ていないと気づけません。アプリを閉じる前に一度ターミナルを読む——バージョン差の大きいライブラリでは、これが最短の点検になります。
本記事のコードには use_container_width を1か所も書いていません。古い書き方を載せて「今は動きます」と補足するより、新しい書き方だけを載せるほうがコピーされたときに壊れません。なお検証したのは 1.58.0 で、pip install streamlit で入るのはこれより新しい版です(PyPIの最新は2026年9月7日時点で1.63.0)。引数や画面が違ったら、python -m streamlit version で自分の版を確認してから公式のリリースノートを見るのが速いです。
6. CSVを受け取る — 第10回・第12回の出力をそのまま食べる
DBに入っていないデータは、CSVで受け取ります。第10回のスクリーナーと第12回のシミュレーターは、結果をCSVに書き出せます。
uploaded = st.file_uploader("CSVを選ぶ", type="csv",
help="第10回・第12回が書き出したCSVは先頭に # の行が付きます")
if uploaded is not None:
df = pd.read_csv(uploaded, comment="#") # # の行を読み飛ばす(記事6章)
st.file_uploader が返すのはファイルパスではなく、メモリ上のファイル風オブジェクトです。pd.read_csv() にそのまま渡せます。第10回の出力を実際に読ませたのが次の画面です。
comment="#" を忘れると、片方は落ちて、もう片方は黙って壊れる
この連載のCSVは、前提や出典を持ち歩くために先頭に # の行が付いています。読み飛ばさないとどうなるかを両方で確かめました。
# 第10回のCSV(先頭に5行のコメント)
pd.read_csv(path)
→ pandas.errors.ParserError: Error tokenizing data. C error: Expected 2 fields in line 6, saw 10
# 第12回のCSV(先頭に1行のコメント)
pd.read_csv(path) → (12, 3) 列名が「# 前提(すべて入力値…): 初期 1」など
pd.read_csv(path, comment="#") → (11, 9) 列名は year, price, dps, gross_dividend, ...
第10回のCSVは例外で止まり、第12回のCSVは例外を出さずに間違った表になります。後者はコメント行がそのまま見出しとして採用され、行の途中のカンマで列が割れ、9列あるはずのデータが3列の表になりました。エラーで止まってくれるほうが、じつは親切です。
どちらのCSVもBOM付き(utf-8-sig)ですが、読み込み側で encoding= の指定は要りません(pandasのパーサがBOMを吸収することを確認)。書き出し側がBOM付きなのはExcelでそのまま開けるようにするためです(第3回)。
名前は紛らわしいのですが、ローカルで streamlit run しているかぎり、送り先は自分のPCで動いているStreamlitサーバーです。ブラウザから localhost に送られ、そこから外には出ません(既定の上限は1ファイル200MB)。この前提が崩れるのは、アプリをどこかのサーバーに置いたときだけです(8章)。
7. 第4〜7回の図と表を、1行で画面に貼る
ここが連載の答え合わせです。第4回は損益一覧を DataFrame で返し、第5回・第7回は savefig せず Figure を返し、第6回の build_dashboard() も Figure を返す形にしました。そのとき「第13回でそのまま渡せる」と書いた約束を確かめます。
第6回のダッシュボードは、本当に「渡すだけ」だった
第6回の最後にはこう書きました——「build_dashboard() が Figure を返す形にしてあるので、そのまま渡すだけで済むはずです」。この「はず」を検証します。
prices = dashboard.make_price_history()
qty, value = dashboard.build_value_history(transactions, prices)
return dashboard.build_dashboard(qty, value, summary), dashboard.CONFIG
st.plotly_chart(fig, width="stretch", config=config, key="sixth")
結果は「渡すだけ」で済みました。変換も再構成も不要で、第6回の CONFIG もそのまま config= に渡せます。積み上げ面グラフ・ドーナツ・横棒の3枚組が、ホバーもレンジ選択ボタンも生きたまま出ました。
build_dashboard() が返した Figure を貼っただけの状態(銘柄・価格・取引はすべて架空)ひとつだけ実測で見つかった罠があります。同じ Figure を key なしで2回貼ると例外です。
streamlit.errors.StreamlitDuplicateElementId: There are multiple plotly_chart elements
with the same auto-generated ID. ... please pass a unique key argument to the plotly_chart element.
Streamlitは要素のIDを引数から自動生成するので、中身が同じ図を2か所に出すと衝突します。key="sixth" のように名前を付ければ解決です。
第4回の表は st.dataframe()、第5回・第7回の図は st.pyplot()
第4回の summary のような DataFrame は、そのまま st.dataframe() に渡せます。列の見せ方も指定しておきました。
DIVIDEND_COLUMNS = {
"paid_on": st.column_config.TextColumn("入金日"),
"name": st.column_config.TextColumn("銘柄"),
"shares": st.column_config.NumberColumn("株数", format="%,d"),
"per_share_yen": st.column_config.NumberColumn("1株あたり", format="yen"),
"net_yen": st.column_config.NumberColumn("入金額", format="yen"),
}
format="yen" は組み込みの指定で ¥1,235 の形になります。"¥%d" と自分で書きたくなりますが、組み込みのほうが桁区切りまで面倒を見てくれます(printf形式なら "%,d" のようにカンマを入れます)。列名はSQL側を英語のまま、見出しだけ日本語にすると対応が崩れません。
第5回・第7回の matplotlib の Figure は st.pyplot(fig) に渡します。ただし本アプリにグラフは同梱していません(この回で新しいグラフを作らないと決めたため)。代わりに差し込み口を空けてあります。
def optional_matplotlib_figure():
sys.path.insert(0, str(APP_DIR))
try:
from my_charts import build_figure
except ModuleNotFoundError:
return None
return build_figure()
同じフォルダに my_charts.py を置き、Figure を返す build_figure() を定義すると画面に出ます。差し込み口がFigure を返す関数なら受け取れることは、この最小例で確認しました。第5回の plot_stock_chart() は Figure をそのまま、第7回の plot_sector_pie() は fig, _ = と受けてから同じ口に渡せます。表示のあとに plt.close(fig) を呼ぶのは、再実行のたびにFigureが積み上がるためです。
第4〜7回はどの関数も「保存する」のではなく「DataFrame か Figure を返す」形で書いてきました。そのおかげで、器をターミナルからブラウザに替えるのに書き足したのは1行ずつ。戻り値の型をそろえておくと、あとから器を替えられる——連載の積み重ねが、ここで回収できました。
8. 動かす — 起動・通信の隔離・そして「公開しない」という選択
起動は streamlit run です。DBのパスは -- のうしろに書いて渡せます(サイドバーで直接入力してもかまいません)。
streamlit run investment_dashboard.py -- --div-db C:/path/dividends.db --perks-db C:/path/perks.db
python investment_dashboard.py と打つと画面は出ません。代わりに警告が並びます。
Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
Warning: to view this Streamlit app on a browser, run it with the following
command:
streamlit run investment_dashboard.py [ARGUMENTS]
壊れているのではなく、Streamlitのランタイムなしで走った(bare mode)というだけです。あわせて Session state does not function when running a script without `streamlit run` も出ます。警告文の中に正しい起動コマンドが書いてあるので、慌てずに読めば済みます。
初回の「Email:」と、待ち受けるアドレス
はじめて streamlit run したときは、ターミナルに Welcome to Streamlit! とメールアドレスの入力欄が出ます。何も入れずEnterでスキップできます(案内文にも空欄でよいと書かれています)。入力するとStreamlit社に送信される仕組みなので、不要なら空のままで問題ありません。聞かれたくない場合やブラウザを自動で開かせたくない場合は --server.headless true を付けます。
起動ログも必ず見てください。こう表示されます(既定のポートは 8501。以下は --server.port 8511 を付けて起動した実行例です)。
You can now view your Streamlit app in your browser.
Local URL: http://localhost:8511
Network URL: http://192.168.x.x:8511
External URL: http://xx.xx.xx.xx:8511
Local だけでなく Network と External のURLが出ています。実際、待ち受けアドレスの既定は未指定=すべてのネットワーク接続を受け付ける状態で、この実行例では 0.0.0.0:8511 でした。同じLANの別の端末からURLを開けるということです。自分のPCの中だけに閉じたいなら、起動時にアドレスを指定します。
streamlit run investment_dashboard.py --server.address localhost --server.port 8511
こうすると表示は URL: http://localhost:8511 の1本になり、待ち受けも 127.0.0.1:8511 になりました(netstat -ano で確認)。保有株数と入金額が並ぶ画面なので、既定のままにしない理由は十分にあります。止めるときはターミナルで Ctrl+C(ブラウザのタブを閉じてもサーバーは動き続けます)。
通信するのは、ボタンを押した1か所だけ
サイドバーの「時価を取得」ボタンだけが、第1回で使った yfinance 経由で外部と通信します。押さなければ、起動から終了まで一度も通信しません(import yfinance もボタンを押したときに初めて実行します)。第9回の --fetch-ex-date・第10回の --fetch と同じ設計です。
取得した値は表示するだけで、保有株数と掛け合わせません。掛けた瞬間に「評価額」という別の性格の数字になり、この画面が資産の査定をしていることになるからです。表示には取得時刻と対象日を添え、yfinanceがYahoo! Financeの非公式ラッパーで値の正確性・遅延・提供の継続は保証されないことも画面に書いています。なお7章で貼った第6回のグラフの「評価額」は、第6回のサンプル価格から計算した架空の推移で、ここで取得した時価とは無関係です。
そして、クラウドには載せない
Streamlitのアプリは、公式のホスティング(Streamlit Community Cloud)に置いてURLで共有できます。本記事はその手順を書きません。理由は公開範囲の決まり方にあります。
Your app will inherit permissions from your GitHub repo, meaning that if your repo is private your app will be private and if your repo is public your app will be public.(Share your app・2026年9月7日確認)
同じページには、公開状態のアプリについて「Once your app is public, just give anyone your app's URL and they view it!」(URLを渡せば誰でも見られる)ともあります。公開範囲を決めるのは、コードを置いたGitHubリポジトリの設定です。公開リポジトリに .db をうっかりコミットすれば、どの銘柄を何株持ち、いくら受け取ったかが外から見える場所に出ます。
このアプリが読むのは、まさにその種類のデータです。自分1人が見るために、外に出す必然性がありません。同じ理由で、DBファイルはクラウド同期フォルダや共有フォルダにも置かないでください。バージョン管理するなら .gitignore に *.db を入れておくのが最低限の防波堤です。画面右上の「Deploy」ボタンが気になるなら --client.toolbarMode minimal で減らせます(本記事の画面はこの設定で撮影)。
9. 部品が1画面になった/このダッシュボードで分からないこと
3〜7章で踏んだ地雷には共通点があります。スレッドの例外以外、どれもエラーを出しません。古いキャッシュは平然と数字を表示し、非推奨の警告はターミナルにだけ流れ、# 行を読み飛ばさなかったCSVは列が3つの表になりました。ダッシュボードは「見えている=正しい」と思わせる力が強いぶん、表示より先に、何をキーに、いつ読んだ値なのかを決めておく必要があります。その場で落ちてくれる KeyError のような例外の読み方はPythonエラー一覧22種と直し方にあります。
この記事の後半は、ほぼ全部が「DataFrameをどう作って、どう渡すか」の話でした。単位を銭から円に直し、列名と見出しを分ける——この手数が増えるほど画面は安定します。3章の桁ずれも6章の3列になったCSVも、つまずきは表示の手前でした。「画面に出す前に、集計・型・欠損を DataFrame 側で決めきる」やり方を一冊で通せるのが、pandasの作者本人が書いたPythonによるデータ分析入門 第3版です(当サイトのおすすめ本ランキング総合5位・全612ページ)。リンク先は当サイトの書籍紹介ページで、アフィリエイト広告を含みます。
この画面が答えること・答えないこと
| 画面の場所 | 答えられること | 答えられないこと |
|---|---|---|
上段の数字(st.metric) | 入力済みの記録の合計と件数 | 記録していない口座の入金・受取の実額 |
| 配当の月別集計 | 入金がどの月に寄っているか | 今年も同じ月に同じ額が入るか |
| 優待の権利確定月 | 登録した必要株数に届いているか | 優待の価値・実施の継続 |
| 第6回のグラフ | 入力した取引と価格から計算した推移 | これから増えるかどうか |
| CSVの表 | そのCSVに何が入っているか | その条件が妥当かどうか |
右の列が、この道具の限界です。税額の正確な計算も、手数料込みの実際の受取額も扱いません。「見える化すれば分かる」とは書けません——見えるのは、自分が入力したものだけです。
次回:株価を毎日ためる(定期実行)
第14回は、Windowsのタスクスケジューラで株価の取得を毎日自動で走らせる回の予定です。連載の全体像は投資×Python シリーズ一覧にあります。
本連載は「自分の資産を自分で管理・可視化するツールを、Pythonの学習題材として自作する」ことが目的です。特定銘柄の売買や投資手法を勧めるものではなく、記事・コード・画面に登場する銘柄名・銘柄コード・株数・株価・配当額・優待の条件はすべて動作確認用の架空の値で、推奨でも予測でもありません。金融商品取引法38条2号は不確実な事項について断定的判断を提供することを禁じており、金融庁の監督指針も「有価証券等の価格、数値、対価の額の動向を断定的に表現したり、確実に利益を得られるように誤解させて、投資意欲を不当に刺激するような表示」を戒めています(金融商品取引業者等向けの総合的な監督指針)。当サイトは投資助言業(金融商品取引業)の登録を行っておらず、個別銘柄の推奨・売買タイミングの助言・税務相談には応じられません。税制の適用や確定申告の要否は国税庁の情報か税理士にご確認ください。投資判断はご自身の責任で(詳細は免責事項)。
10. よくある質問(FAQ)
Q. 第8回・第11回のDBは書き換わりますか?
書き換わりません。2つのDBは file:...?mode=ro の読み取り専用モードで開いており、書き込もうとすると sqlite3.OperationalError: attempt to write a readonly database になります(3章)。ファイルが無いときも新しいDBを作らずに例外で止まります。書き込みが要る作業は、これまでどおり第8回・第11回のツールで行ってください。
Q. st.cache_data が更新されない(DBに入力したのに画面の数字が変わりません)
st.cache_data が古い結果を返しているためです。判定材料は「入力パラメータの値」と「関数の中のコード」の2つだけで、DBファイルが書き換わったことは入っていません(公式ドキュメント)。対処は、更新時刻を引数に渡す・ttl= を付ける・st.cache_data.clear() をボタンに割り当てる、の3つです(4章)。本アプリは1つ目を既定にしています。
Q. use_container_width で警告が出ます
streamlit 1.58.0 ですでに非推奨です。use_container_width=True は width="stretch"、False は width="content" に置き換えてください。警告文には削除予定日として 2025-12-31 と書かれています。この警告はブラウザではなくターミナルにだけ出るので、気づかないまま古い書き方が残りがちです(5章)。
Q. python investment_dashboard.py で実行すると画面が出ません
Streamlitのアプリは streamlit run で起動します。python で直接実行するとランタイムが無い状態(bare mode)で走り、missing ScriptRunContext! の警告と正しい起動コマンドの案内が出ます(8章)。処理自体は最後まで走るので、エラーで落ちているわけではありません。
Q. 初回起動で「Email:」と聞かれて止まります
Streamlitの初回起動時の案内です。何も入力せずEnterを押せば先に進みます(案内文にも空欄でよいとあります)。入力するとStreamlit社に送信されるので、不要なら空のままで問題ありません。今後表示させたくない場合は --server.headless true を付けて起動します(8章)。
Q. スマホや外出先から見られるようにできますか?
本記事では扱いません。外から見えるようにするには、アプリをどこかのサーバーに置くか自分のPCを外部に公開することになり、保有銘柄と金額が自分のPCの外に出ます。本記事は逆方向で、--server.address localhost を付けて同じLANの別端末からも開けない状態にすることを勧めています(8章)。既定では 0.0.0.0 で待ち受け、起動ログにNetwork URLとExternal URLが出ます。
Q. Streamlit Community Cloud に上げてもいいですか?
本記事のデータを載せることは勧めません。公式ドキュメントは「アプリはGitHubリポジトリの権限を継承する。リポジトリが公開ならアプリも公開になる」と説明しており、公開状態のアプリはURLを知っていれば誰でも開けます(8章に原文)。このアプリが読むのは保有銘柄・株数・入金額です。.db をコミットしない、.gitignore に *.db を入れる——そこまでやってもリスクを背負う理由が見当たらないので、手元で動かしてください。
Q. このダッシュボードを見て、買う銘柄を決められますか?
決められません。表示しているのは、自分で入力した記録の合計・件数・内訳だけです(9章の表)。st.metric に増減の矢印や赤緑の色をあえて付けていないのも、表示が評価に見えることを避けるためです。投資判断はご自身の責任でお願いします。
まずは完成版 investment_dashboard.py を保存し、第8回・第11回のDBを指定して起動するところまで。そのあとサイドバーの「DBの更新時刻を見て読み直す」を外し、第8回のツールで1件入力すると、4章の画面が自分の手で再現できます。第8回・第11回のDBが手元になくても、DBの欄を空のままにすれば6章のCSV読み込みだけでも動く構成です。自分の別のDBを見たいなら、配布コード先頭のSQLをテーブル名と列名に合わせて書き換えます。
記録を貯める側は第8回、貼り付けた図の作り方は第6回、pandasやPlotlyの位置づけはよく使うPythonライブラリ51選にあります。連載の全体像は投資×Python シリーズ一覧(ブックマーク推奨)か新着記事のRSSで追えます。
本記事は、生成AIを活用して下書きし、運営者が内容を確認・編集したうえで公開しています。掲載しているコード・出力・画面は Windows 11 / Python 3.12.10 / streamlit 1.58.0 / pandas 3.0.3 / plotly 6.8.0 で 2026年9月7日に実行して確認したものです。記事・コード・画面に登場する銘柄名・銘柄コード・株数・株価・配当額・優待の条件はすべて動作確認用の架空の値であり、推奨でも予測でもありません。本アプリが表示するのは利用者が入力した記録の集計であって、資産の査定でも投資判断でもありません。本記事はプログラミングの情報提供を目的としたもので、銘柄の売買・投資手法の推奨でも税務上の助言でもありません。投資判断はご自身の責任で。AIの利用方針は免責事項をご覧ください。