
「副業でも評価される軽い成果物、何を作ればいい?」
自分のデータ×外部APIをつないだ“ミニアプリ”が王道です。
本記事では、家計簿(CSV)と天気APIをつないで、月次/カテゴリ別の支出×気象(平均気温・降水量)をStreamlitで可視化するテンプレートを配布します。APIコールの作法、SQLiteでの永続化、UIの比較・注釈まで、コピペで動かせる形に整えました。社内レポートの簡易ダッシュボードや副業のお試し納品にも、そのまま流用できます。
この記事で身に付く力
- APIを“丁寧に”呼ぶ設計力(タイムアウト/上限/バックオフ/キャッシュ)
- 軽量DB(SQLite)でデータを貯める運用力(スナップショット・去重)
- 意思決定に繋がるUIの作り方(比較・関係・注釈)
- 配布できる形(Streamlit+スケジューリングで“毎日動く”)
ふみとの実体験:社内PoCで「家計×気象」の簡易可視化を試したところ、施策会議の冒頭5分で“話が早い”状態を作れました。ポイントは、前処理やUIに凝り過ぎず、再現できる型に落とし込むこと。この記事はその型を、最短距離で置いていきます。
関連記事:
>>API入門:OpenAPI/HTTPの基本と“壊れない”Pythonクライアント設計(コピペOK)
>>自動化:スケジューリングと業務改善の型|「再実行安全×観測可能×静かに動く」を仕組みにする
>>【保存版】SQLite×Pythonで作る“ローカルDWH”——ETL・集計・レポート自動化の最短手順
>>もう事故らせない:PythonでCSV/JSON/Excelを安全に読み書きする実務レシピ
>>【保存版】可視化入門:Matplotlib/Plotlyの使い分けと“伝わるグラフ設計”10ステップ
>>コピペで回るレポート納品|Jupyter→PDF/HTML→共有の自動化テンプレ
まず押さえるべき3つの落とし穴
ミニアプリが“動くけど使われない”原因はシンプルです。API呼び出しが雑(上限無視/無限リトライ)、使い捨て実装(CSV→グラフで毎月やり直し)、弱いUI(比較や注釈がなく判断につながらない)。本稿のテンプレは、これらを設計→実装→運用の順で解消します。
最小アーキテクチャと依存パッケージ
weather-budget-app/
app/
__init__.py
config.py # .env読み込み
api_client.py # 天気APIクライアント(丁寧なGET)
ingest_budget.py # 家計簿CSV取込
ingest_weather.py# 天気の取得・保存
repo.py # SQLiteアクセス(CRUD)
ui_app.py # Streamlit UI
data/
raw/ # 元CSV(家計簿)
db.sqlite3 # 永続DB
cache/ # APIレスポンスキャッシュ
tests/
.env.example
requirements.txt
README.md
python-dotenv>=1.0
requests>=2.32
pandas>=2.2
sqlalchemy>=2.0
streamlit>=1.36
plotly>=5
実装の指針(重要)
- APIはタイムアウト必須+429は上限付きバックオフ+キャッシュ
- データはSQLiteで永続化し、重複挿入を防止
- UIは比較(積み上げ×折れ線)と関係(散布図)を1画面に
設定:.envとConfig
API_BASE=https://api.example-weather.com
API_KEY=your_api_key_here
LAT=35.6895
LON=139.6917
TIMEZONE=Asia/Tokyo
実運用では利用規約/レート制限/二次利用条件を確認のうえでキーを管理しましょう。→ [内部リンク:API入門:OpenAPI/HTTPの基本と活用例]
from dataclasses import dataclass
from dotenv import load_dotenv
import os
load\_dotenv()
@dataclass(slots=True)
class Settings:
api\_base: str = os.getenv("API\_BASE", "")
api\_key: str | None = os.getenv("API\_KEY")
lat: float = float(os.getenv("LAT", 35.6895))
lon: float = float(os.getenv("LON", 139.6917))
tz: str = os.getenv("TIMEZONE", "Asia/Tokyo")
SET = Settings()
丁寧なGET:天気APIクライアント(キャッシュ付き)
from __future__ import annotations
import json, time, hashlib
from pathlib import Path
import requests
from typing import Any
from .config import SET
CACHE = Path("data/cache"); CACHE.mkdir(parents=True, exist\_ok=True)
UA = "pythonbunseki-weather/1.0 (+[https://pythonbunseki.com](https://pythonbunseki.com))"
class ApiError(Exception): ...
def cache\_path(url: str, params: dict\[str, Any]) -> Path:
key = url + "?" + "&".join(f"{k}={v}" for k, v in sorted(params.items()))
h = hashlib.sha256(key.encode()).hexdigest()\[:16]
return CACHE / f"{h}.json"
def get\_json(path: str, params: dict\[str, Any]) -> dict:
url = SET.api\_base.rstrip("/") + "/" + path.lstrip("/")
params = {\*\*params}
headers = {"User-Agent": UA, "Authorization": f"Bearer {SET.api\_key}"} if SET.api\_key else {"User-Agent": UA}
cp = cache\_path(url, params)
if cp.exists():
return json.loads(cp.read\_text(encoding="utf-8"))
tries = 0
while True:
tries += 1
r = requests.get(url, params=params, headers=headers, timeout=10)
if r.status\_code == 429:
if tries >= 3:
raise ApiError("rate limited")
time.sleep(min(8, 0.5 \* 2 \*\* (tries-1)))
continue
r.raise\_for\_status()
data = r.json()
cp.write\_text(json.dumps(data, ensure\_ascii=False), encoding="utf-8")
return data
家計簿CSVの取り込み(正規化・名寄せ)
import pandas as pd
from pathlib import Path
from sqlalchemy import create_engine
COLS = {"date": "date", "category": "category", "amount": "amount"}
MAP = {
"食費": "食費", "外食": "食費", "カフェ": "食費",
"交通": "交通", "タクシー": "交通",
"日用品": "日用品", "ドラッグストア": "日用品",
}
def read\_budget\_csv(p: Path) -> pd.DataFrame:
df = pd.read\_csv(p, encoding="utf-8", parse\_dates=\[COLS\["date"]])
df = df.rename(columns=COLS)\[\["date", "category", "amount"]]
df\["amount"] = df\["amount"].astype("float")
df\["norm\_cat"] = df\["category"].map(MAP).fillna(df\["category"])
df\["date"] = df\["date"].dt.date
return df
def upsert\_budget(df: pd.DataFrame, db\_path="data/db.sqlite3") -> None:
eng = create\_engine(f"sqlite:///{db\_path}")
df = df.drop\_duplicates(subset=\["date", "norm\_cat", "amount"]).copy()
df.to\_sql("budget", eng, if\_exists="append", index=False)
メモ/店名などのPIIは保存しない方針が安全です。必要ならハッシュ化して扱いましょう。
天気の取得と保存(SQLite)
import pandas as pd
from datetime import date
from sqlalchemy import create_engine
from .config import SET
from .api_client import get_json
# 例: /history?lat=..\&lon=..\&start=YYYY-MM-DD\&end=YYYY-MM-DD
def fetch\_daily\_weather(d0: date, d1: date) -> pd.DataFrame:
js = get\_json("/history", {
"lat": SET.lat, "lon": SET.lon,
"start": d0.isoformat(), "end": d1.isoformat(),
"tz": SET.tz
})
recs = \[]
for r in js.get("days", \[]):
recs.append({
"date": pd.to\_datetime(r\["date"]).date(),
"tavg": float(r.get("tavg", 0)),
"prcp": float(r.get("prcp", 0)),
})
return pd.DataFrame(recs)
def upsert\_weather(df: pd.DataFrame, db\_path="data/db.sqlite3") -> None:
eng = create\_engine(f"sqlite:///{db\_path}")
df.to\_sql("weather", eng, if\_exists="append", index=False)
CREATE TABLE IF NOT EXISTS budget (
date TEXT, norm_cat TEXT, amount REAL
);
CREATE TABLE IF NOT EXISTS weather (
date TEXT PRIMARY KEY, tavg REAL, prcp REAL
);
重複INSERTを避けるために、weather.date
を主キーにする運用が手堅いです。
結合ビューと集計
import pandas as pd
from sqlalchemy import create_engine
ENG\_STR = "sqlite:///data/db.sqlite3"
def load\_joined() -> pd.DataFrame:
eng = create\_engine(ENG\_STR)
q = """
SELECT b.date, b.norm\_cat AS category, b.amount,
w\.tavg, w\.prcp
FROM budget b
LEFT JOIN weather w USING(date)
"""
return pd.read\_sql(q, eng, parse\_dates=\["date"])
def agg\_monthly(df: pd.DataFrame) -> pd.DataFrame:
g = df.copy()
g\["ym"] = g\["date"].dt.to\_period("M").astype(str)
return g.groupby(\["ym", "category"], as\_index=False).agg(
spend=("amount","sum"),
prcp=("prcp","mean"),
tavg=("tavg","mean")
)
Streamlit UI:比較と関係を1画面に
import streamlit as st
import pandas as pd
import plotly.express as px
from app.repo import load_joined, agg_monthly
st.set\_page\_config(page\_title="天気×家計簿", layout="wide")
st.title("天気×家計簿の自動可視化")
@st.cache\_data(ttl=300)
def get\_data():
df = load\_joined()
return df.dropna(subset=\["amount"])
df = get\_data()
if df.empty:
st.warning("データがありません。家計簿CSV取り込みと天気取得を先に実行してください。")
st.stop()
cats = sorted(df\["category"].dropna().unique().tolist())
sel = st.multiselect("カテゴリを選択", cats, default=cats\[:3])
period = st.slider("期間(月)", 3, 24, 12)
latest = df\["date"].max()
from\_date = (latest - pd.DateOffset(months=period)).normalize()
f = df\[(df\["category"].isin(sel)) & (df\["date"] >= from\_date)]
monthly = agg\_monthly(f)
col1, col2 = st.columns(2)
with col1:
st.subheader("月次支出(カテゴリ別)")
fig1 = px.bar(monthly, x="ym", y="spend", color="category", barmode="stack")
fig1.update\_layout(showlegend=True, xaxis\_title="月", yaxis\_title="支出")
st.plotly\_chart(fig1, use\_container\_width=True)
with col2:
st.subheader("平均気温と降水量(同月平均)")
fig2 = px.line(monthly.groupby("ym", as\_index=False).mean(numeric\_only=True), x="ym", y=\["tavg","prcp"])
fig2.update\_layout(xaxis\_title="月", yaxis\_title="値")
st.plotly\_chart(fig2, use\_container\_width=True)
st.subheader("支出×天気の関係(散布図)")
sc = monthly.groupby("ym", as\_index=False).agg(spend=("spend","sum"), prcp=("prcp","mean"), tavg=("tavg","mean"))
fig3 = px.scatter(sc, x="tavg", y="spend", size="prcp", hover\_name="ym")
fig3.update\_layout(xaxis\_title="平均気温", yaxis\_title="総支出", legend\_title="降水量(点サイズ)")
st.plotly\_chart(fig3, use\_container\_width=True)
st.caption("注:気象データは地点平均。月内のイベント(ボーナス/旅行等)は別途注釈を。")
手動から自動へ:スケジューリング
最低限の運用は、家計簿CSVを月初に取り込み、天気は日次で前日分を取得する流れです。UIは必要時に起動します。
# 天気を毎朝6:00に更新
0 6 * * * cd /path/to/weather-budget-app && /usr/bin/python -m app.ingest_weather >> logs/$(date +\%Y\%m\%d).log 2>&1
Windowsタスクスケジューラの設定は → [内部リンク:自動化:スケジューリングと業務改善の型] を参照。
品質と運用のチェックリスト
- API規約:レート/二次利用/帰属表記を確認。不可なら取得をやめる。
- 時刻/タイムゾーン:保存は
Asia/Tokyo
で統一。 - 去重:家計簿の重複行、天気の日付重複を排除。
- 再現性:
requirements.txt
固定。Docker化は [内部リンク:Docker超入門]。 - PII:個人情報は持ち込まない。必要時はハッシュ化。
- ロギング:JSON LinesでAPI呼び出し/件数/所要時間を記録。→ [内部リンク:例外処理とログ設計]
活用シーンのヒント
家計×行動:暑い月ほど「外食/カフェ」が増える傾向を可視化。翌月の予算配分や節約施策の検討材料に。
EC×気象:在庫/売上データと降水量の相関をチェック。雨天プロモの効果検証へ拡張できます。
店舗運営:最高/最低気温×来店数の関係から、シフト最適化の足がかりを得る。
今日やること(90分)
- リポジトリ雛形を作成し、
requirements.txt
をインストール。 .env
にAPIベースURL/キー/緯度経度を設定(規約確認)。- 家計簿CSVを
data/raw/
へ置き、ingest_budget.py
を実行。 ingest_weather.py
で過去30日を取得→SQLite保存。streamlit run app/ui_app.py
でダッシュボードを確認。- 余力があればcrontab/Windowsタスクで自動更新。
まとめ:小さく作り、静かに価値を積む
“毎月作り直し”を終わらせる鍵は、丁寧なGET・SQLiteで貯める・比較と注釈のあるUI。このテンプレから始めて、あなたのデータに合わせて拡張してください。次は、通知やバッチ処理を足して“毎日回る”小さな仕組みにしていきましょう。
伴走:提出できる“ミニアプリ”まで
無料カウンセリング/体験で、API選定→実装→DB→UI→運用まで一緒に仕上げます。「やらない判断」も含めて最短ルートで。
TechAcademy データサイエンスコース(受講料:174,600円~ ※更に割引あり)

株式会社キカガク AI人材長期育成コース(受講料:237,600円~)

この記事から次に読むべきもの(内部リンク)
-
-
API入門:OpenAPI/HTTPの基本と“壊れない”Pythonクライアント設計(コピペOK)
API連携を始めたいけど、何から学べば“壊れない仕組み”になる? OpenAPI?HTTP?タイムアウト?……用語が多すぎて迷子になりがち。 本記事は、HTTPの基礎×OpenAPIの読み方×堅牢なク ...
-
-
自動化:スケジューリングと業務改善の型|「再実行安全×観測可能×静かに動く」を仕組みにする
夜中に動かしているPython、自動で止まってた…ログもなくて原因が追えない…。 「毎朝のレポート」や「在庫監視」を、壊れず静かに回したい…! 業務で落ちない自動化を作る鍵は、(1) 再実行安全(Id ...
-
-
【保存版】SQLite×Pythonで作る“ローカルDWH”——ETL・集計・レポート自動化の最短手順
ローカルでゼロ構築、ファイル1つで完結、サーバ不要。本記事はSQLite×Pythonで“毎日回る”ETL・集計・レポート自動化を最短で作るための完全ガイドです。データ設計→DB作成→ETL(取り込み ...
-
-
【保存版】可視化入門:Matplotlib/Plotlyの使い分けと“伝わるグラフ設計”10ステップ
結論:可視化は「きれいに描く」ことではなく、意思決定を動かすための設計です。本稿では、未経験〜初学者が 週10時間×1〜2週 で、Matplotlib/Plotlyを軸に “伝わるグラフ”の設計と実装 ...
-
-
コピペで回るレポート納品|Jupyter→PDF/HTML→共有の自動化テンプレ
毎週のレポート納品、朝にバタつきませんか? コードや図表は作ったのに、PDF化や共有で崩れる…。その“揺らぎ”を今日で終わらせましょう。 分析の価値は、最後の“納品物”で決まります。本記事では、Jup ...
最近のコメント