実案件/ポートフォリオ

WebAPI連携ミニアプリ:天気×家計簿の自動可視化|“取る→貯める→見せる”を半日で

「副業でも評価される軽い成果物、何を作ればいい?」

自分のデータ×外部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分)

  1. リポジトリ雛形を作成し、requirements.txtをインストール。
  2. .envにAPIベースURL/キー/緯度経度を設定(規約確認)。
  3. 家計簿CSVをdata/raw/へ置き、ingest_budget.pyを実行。
  4. ingest_weather.pyで過去30日を取得→SQLite保存。
  5. streamlit run app/ui_app.pyでダッシュボードを確認。
  6. 余力があればcrontab/Windowsタスクで自動更新。

まとめ:小さく作り、静かに価値を積む

“毎月作り直し”を終わらせる鍵は、丁寧なGETSQLiteで貯める比較と注釈のあるUI。このテンプレから始めて、あなたのデータに合わせて拡張してください。次は、通知やバッチ処理を足して“毎日回る”小さな仕組みにしていきましょう。

伴走:提出できる“ミニアプリ”まで

無料カウンセリング/体験で、API選定→実装→DB→UI→運用まで一緒に仕上げます。「やらない判断」も含めて最短ルートで。

TechAcademy データサイエンスコース(受講料:174,600円~ ※更に割引あり)

TechAcademy 無料相談

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

キカガク 無料相談

この記事から次に読むべきもの(内部リンク)

API入門
API入門:OpenAPI/HTTPの基本と“壊れない”Pythonクライアント設計(コピペOK)

API連携を始めたいけど、何から学べば“壊れない仕組み”になる? OpenAPI?HTTP?タイムアウト?……用語が多すぎて迷子になりがち。 本記事は、HTTPの基礎×OpenAPIの読み方×堅牢なク ...

自動化
自動化:スケジューリングと業務改善の型|「再実行安全×観測可能×静かに動く」を仕組みにする

夜中に動かしているPython、自動で止まってた…ログもなくて原因が追えない…。 「毎朝のレポート」や「在庫監視」を、壊れず静かに回したい…! 業務で落ちない自動化を作る鍵は、(1) 再実行安全(Id ...

SQLite
【保存版】SQLite×Pythonで作る“ローカルDWH”——ETL・集計・レポート自動化の最短手順

ローカルでゼロ構築、ファイル1つで完結、サーバ不要。本記事はSQLite×Pythonで“毎日回る”ETL・集計・レポート自動化を最短で作るための完全ガイドです。データ設計→DB作成→ETL(取り込み ...

可視化
【保存版】可視化入門:Matplotlib/Plotlyの使い分けと“伝わるグラフ設計”10ステップ

結論:可視化は「きれいに描く」ことではなく、意思決定を動かすための設計です。本稿では、未経験〜初学者が 週10時間×1〜2週 で、Matplotlib/Plotlyを軸に “伝わるグラフ”の設計と実装 ...

レポート納品
コピペで回るレポート納品|Jupyter→PDF/HTML→共有の自動化テンプレ

毎週のレポート納品、朝にバタつきませんか? コードや図表は作ったのに、PDF化や共有で崩れる…。その“揺らぎ”を今日で終わらせましょう。 分析の価値は、最後の“納品物”で決まります。本記事では、Jup ...

最近のコメント

    • この記事を書いた人
    • 最新記事

    ふみと

    このブログでは、データサイエンティストとして市場価値を上げる方法を独自にまとめて発信しています。

    【プロフィール】
    ・大手企業データサイエンティスト/マーケティングサイエンティスト(10年、年収900万円台)/案件100件以上
    ・資格:JDLA E資格(日本ディープラーニング協会主催)/JDLA Community(CDLE会員)/Advanced Marketer/ビジネス統計スペシャリスト/統計検定2級/TOEIC 805
    ・スキル:Python/Tableau/SQL/機械学習/Deep Learning/RPA

    -実案件/ポートフォリオ