Python基礎

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

ローカルでゼロ構築、ファイル1つで完結、サーバ不要。本記事はSQLite×Pythonで“毎日回る”ETL・集計・レポート自動化を最短で作るための完全ガイドです。データ設計→DB作成→ETL(取り込み/整形/検証)→インデックス/PRAGMAによる高速化→pandas/Matplotlib/ExcelWriterでの配布まで、事故らない前処理の型として落とし込みます。最後に増分更新パイプライン自動実行(タスクスケジューラ/cron)の雛形、そしてレビュー観点チェックリストも配布します。

この記事で身に付く力

  • 軽量DWH(SQLite)設計とETL自動化:ローカル完結で安全・再現性の高い基盤づくり
  • 高速化の実務勘所:PRAGMA/インデックス/バルク挿入の使いどころ
  • 配布までの一気通貫:pandas+ExcelWriter+画像出力の納品パターン

ふみとの現場メモ

大手でマーケ分析を回していた頃、Excel 6ブック×40シートの手作業更新で毎月1時間×複数人が溶けていました。SQLiteに取り込み→SQLで整形→ExcelWriterで配布に切り替えたところ、更新は5分&再現性100%に。小さくてもDWH化は効きます。

SQLite×Pythonで何が解決できる?

CSVのまま複数JOINで破綻、Notebookに前処理コピペが散乱、毎月のExcel納品でコピペ地獄…こうした「あるある」を、SQLiteを軽量DWHとして採用し、PythonでETLと配布を自動化することで一掃します。ファイル1つにクエリと再現性を閉じ込めましょう。

使用環境

Python 3.10+ / pandas 2.x / SQLite3(標準ライブラリ) を想定。Windows/Mac/Linuxいずれでも動作します。

プロジェクト構成(雛形)

retail_etl/
  ├─ data/
  │   ├─ raw/           # 受領データ(CSV等)
  │   └─ warehouse/     # SQLite DB配置
  ├─ src/
  │   ├─ etl.py         # 取り込み/整形
  │   ├─ sql/           # SQLテンプレ
  │   │   ├─ create_tables.sql
  │   │   └─ queries.sql
  │   └─ report.py      # 集計→Excel/画像出力
  ├─ reports/
  ├─ requirements.txt
  └─ README.md

スキーマ設計 → SQLite DBを作る

例として sales(order_date, store, product, qty, price)stores(store, area, open_date)主キー/UNIQUE制約で重複挿入を物理的に防ぎ、複合インデックスはフィルタ/結合列へ。

PRAGMA foreign_keys=ON;
CREATE TABLE IF NOT EXISTS sales (
  id         INTEGER PRIMARY KEY AUTOINCREMENT,
  order_date TEXT NOT NULL,
  store      TEXT NOT NULL,
  product    TEXT NOT NULL,
  qty        INTEGER NOT NULL,
  price      REAL    NOT NULL,
  UNIQUE(order_date, store, product, qty, price)
);
CREATE TABLE IF NOT EXISTS stores (
  store     TEXT PRIMARY KEY,
  area      TEXT,
  open_date TEXT
);
CREATE INDEX IF NOT EXISTS idx_sales_store_date ON sales(store, order_date);

DB初期化スクリプト(Python)

WALで同時読取に強くし、foreign_keys=ONで参照整合性を担保します。

from pathlib import Path
import sqlite3

ROOT = Path(**file**).resolve().parents\[1]
DB = ROOT / "data/warehouse/retail.sqlite"
SQL\_DIR = ROOT / "src/sql"

def init\_db() -> None:
DB.parent.mkdir(parents=True, exist\_ok=True)
con = sqlite3.connect(DB)
try:
con.execute("PRAGMA journal\_mode=WAL;")  # 高速/同時読み取り
con.execute("PRAGMA synchronous=NORMAL;")
con.execute("PRAGMA foreign\_keys=ON;")
con.executescript((SQL\_DIR / "create\_tables.sql").read\_text(encoding="utf-8"))
finally:
con.close()

取り込み(CSV→SQLite)とバリデーション

欠損・型・範囲の検査を取り込み前に行い、壊れた行を入れません。

import pandas as pd, numpy as np, sqlite3
from pathlib import Path

ROOT = Path(**file**).resolve().parents\[1]
DB = ROOT / "data/warehouse/retail.sqlite"

def load\_csv\_to\_db(csv\_path: str) -> None:
con = sqlite3.connect(DB)
try:
df = pd.read\_csv(csv\_path)
\# バリデーション(最小限)
req = {"order\_date","store","product","qty","price"}
assert req.issubset(df.columns)
df = df.dropna(subset=\["order\_date","store","product"])
df\["qty"] = pd.to\_numeric(df\["qty"], errors="coerce").fillna(0).astype(int)
df\["price"] = pd.to\_numeric(df\["price"], errors="coerce").astype(float)
df = df\[df\["qty"] > 0]
\# 追記
df.to\_sql("sales", con, if\_exists="append", index=False)
finally:
con.close()

安全なINSERT(DB-APIのプレースホルダ)

import sqlite3
from pathlib import Path

DB = Path(**file**).resolve().parents\[1] / "data/warehouse/retail.sqlite"

def insert\_store(store: str, area: str, open\_date: str) -> None:
con = sqlite3.connect(DB)
try:
con.execute("PRAGMA foreign\_keys=ON;")
with con:  # トランザクション
con.execute(
"INSERT OR REPLACE INTO stores(store, area, open\_date) VALUES(?,?,?)",
(store, area, open\_date)
)
finally:
con.close()

集計クエリを関数化(重い整形はSQL、配布はpandas)

import sqlite3, pandas as pd
from pathlib import Path

DB = Path(**file**).resolve().parents\[1] / "data/warehouse/retail.sqlite"

Q\_MONTHLY = """
WITH monthly AS (
SELECT strftime('%Y-%m', order\_date) AS ym, store,
SUM(qty\*price) AS sales, SUM(qty) AS qty
FROM sales
WHERE qty>0 AND price>0
GROUP BY ym, store
)
SELECT \* FROM monthly;
"""

def fetch\_monthly() -> pd.DataFrame:
con = sqlite3.connect(DB)
try:
return pd.read\_sql\_query(Q\_MONTHLY, con)
finally:
con.close()

レポート出力(Excel/画像)

集計→図の保存→Excel出力までをワンショットで。

from pathlib import Path
import matplotlib.pyplot as plt
from queries import fetch_monthly

ROOT = Path(**file**).resolve().parents\[1]

def export\_report() -> None:
df = fetch\_monthly()
\# 可視化(例:月次売上推移)
g = df.groupby("ym", as\_index=False)\["sales"].sum()
plt.figure(); plt.plot(g\["ym"], g\["sales"], marker="o")
plt.title("月次売上の推移"); plt.xticks(rotation=45); plt.tight\_layout()
fig\_path = ROOT / "reports/monthly\_sales.png"
plt.savefig(fig\_path, dpi=200, bbox\_inches="tight")

```
# Excel書き出し
xlsx = ROOT / "reports/report.xlsx"
xlsx.parent.mkdir(parents=True, exist_ok=True)
with pd.ExcelWriter(xlsx) as xw:
    df.to_excel(xw, sheet_name="monthly", index=False)
print("Saved:", xlsx, fig_path)

```

Excelには定義シート(SQL/前提)を同梱すると親切です。

増分更新(差分ロード)の型

import sqlite3, pandas as pd
from pathlib import Path

DB = Path(**file**).resolve().parents\[1] / "data/warehouse/retail.sqlite"

def incremental\_load(csv\_path: str) -> None:
con = sqlite3.connect(DB)
try:
cur = con.execute("SELECT IFNULL(MAX(order\_date),'0000-00-00') FROM sales")
last = cur.fetchone()\[0]
df = pd.read\_csv(csv\_path)
df = df\[df\["order\_date"] > last]
if not df.empty:
df.to\_sql("sales", con, if\_exists="append", index=False)
finally:
con.close()

最大日付/シーケンスで差分を挿入。重複はUNIQUE制約で防御します。

性能チューニング:PRAGMA/インデックス/バルク

  • PRAGMA journal_mode=WAL;:同時読取/書込の体感向上
  • PRAGMA synchronous=NORMAL;:書込速度と安全性のバランス
  • インデックスWHERE/JOINで使う列(例:store, order_date)。複合は選択性→範囲の順
  • バルク挿入executemanyあるいはpandas.to_sql(chunksize=…)
  • VACUUM:削除後にファイル縮小

品質保証:簡易データチェック&テスト

import pandas as pd, sqlite3
from pathlib import Path

DB = Path(**file**).resolve().parents\[1] / "data/warehouse/retail.sqlite"

def check\_sales() -> None:
con = sqlite3.connect(DB)
try:
df = pd.read\_sql\_query("SELECT qty, price FROM sales", con)
assert (df\["qty"] >= 0).all(), "qty に負値あり"
assert (df\["price"] >= 0).all(), "price に負値あり"
finally:
con.close()

スケジューリング(自動実行)の最短メモ

  • Windows:タスクスケジューラで python src/etl.py を毎朝7:00
  • macOS/Linuxcrontab -e0 7 * * * /usr/bin/python3 /path/src/etl.py
  • 失敗時はログ/通知(メール/Slack)で安心運用

エラーハンドリング&ロールバック

import logging, sqlite3
from pathlib import Path

logging.basicConfig(level=logging.INFO)
DB = Path(**file**).resolve().parents\[1] / "data/warehouse/retail.sqlite"

def safe\_insert(rows: list\[tuple]) -> None:
con = sqlite3.connect(DB)
try:
with con:
con.executemany(
"INSERT INTO sales(order\_date,store,product,qty,price) VALUES(?,?,?,?,?)",
rows
)
except Exception as e:
logging.exception("insert failed: %s", e)  # 自動でROLLBACK
finally:
con.close()

小さく始めて広げる(スケールの道筋)

  • ファイル1つ→部門共有ならOneDrive/Driveに置いて共有(ロック注意)
  • 行数が増えたら抽出→Parquetを併用 [内部リンク:pandas実践]
  • 同じSQLをPostgreSQLへ移植(方言差:日付関数/IFNULL→COALESCEなど)

次の一歩:無料カウンセリングで設計レビュー

独学でも到達できますが、設計/クエリ/増分更新/配布の仕上げはレビューが最短です。6ヶ月ロードマップでポートフォリオまで作るなら、下記2校の無料カウンセリングから設計レビューを受けましょう。

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

TechAcademy 無料相談

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

キカガク 無料相談

読者タイプ別の運用プラン

  • 社会人(転職狙い)増分ETL→ウィンドウ→Excel配布の一連をポートフォリオ化。READMEに再現手順を明記。→ [内部リンク:ポートフォリオ完全ガイド]
  • 副業(レポート納品)テンプレSQL+ExcelWriterで毎月自動。差分ロードで納期短縮。→ [内部リンク:データレポート納品の型]
  • 主婦/夫(在宅):短時間×高頻度でCTE分割→小関数化→スケジューラ導入の3段階で拡張。

行動課題:ミニプロジェクト(提出推奨)

課題:「sales.csv をSQLiteに取り込み、月次×店舗の売上/数量前年比report.xlsx に出力。画像1枚(折れ線+注釈)を添付」

  • init_db()でDB作成→load_csv_to_db()で取り込み
  • fetch_monthly()で集計、Matplotlibで折れ線+注釈(キャンペーン開始等)
  • ExcelWriterでmonthlyタブを書き出し
  • READMEに再現手順/依存を書き、reports/をGitにコミット

レビュー観点チェックリスト(コピペ可)

  • [ ] 主キー/UNIQUEで重複を防いでいる
  • [ ] PRAGMA/INDEXで速度を確保している
  • [ ] 取り込み前に型/欠損/範囲を検査している
  • [ ] CTEでクエリを段階化し、可読性を確保
  • [ ] 再現手順をREADMEに明記

付録A:よく使うSQLスニペット(SQLite)

-- Top-N per group
SELECT * FROM (
  SELECT *, ROW_NUMBER() OVER(PARTITION BY store ORDER BY sales DESC) rn
  FROM monthly
) WHERE rn <= 3;

\-- IFNULL/COALESCE
SELECT COALESCE(area, 'Unknown') FROM stores;

\-- 日付操作(文字列→月)
SELECT strftime('%Y-%m', order\_date) AS ym FROM sales;

付録B:in-memory DBでのテスト

import sqlite3, pandas as pd
con = sqlite3.connect(":memory:")
con.executescript("CREATE TABLE t(a INT); INSERT INTO t VALUES(1),(2);")
print(pd.read_sql_query("SELECT COUNT(*) AS n FROM t", con))
con.close()

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

SQL
はじめてのSQL:SELECT/WHERE/GROUP BYを最短で理解【コピペOK】

データ分析・自動レポート・簡易アプリの土台はSQLです。Pythonだけで押し切るより、前処理の7割をDB側で完結させる方が速く・安定します。本記事は、未経験〜初学者が週10時間×2〜3週で、SELE ...

スキルアップ
【実務で差がつく】pandas実践:欠損処理・結合・ウィンドウ関数・時系列・品質保証まで“読みやすく速い”型を習得

リード(結論)基礎を終えたら次は実務の現場で頻出する処理を“型”で覚える段階です。本記事は、pandas 2.x を前提に、欠損・外れ値・結合・ウィンドウ関数・時系列・カテゴリ処理・集計の自動化・大規 ...

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

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

データレポート納品
【保存版】データレポート納品の型:要件定義→ETL→検証→可視化→Excel/PDF→引き継ぎまで、失注しないワークフロー完全版

“いい分析”より“伝わる納品”。副業や実務で評価されるのは、意思決定に効く1枚と再現できるパッケージを期限通り出せること。 本記事は、未経験〜初学者が週10時間×2〜3週で、要件定義→データ受領→ET ...

ポートフォリオ
【保存版】データ職のポートフォリオ完全ガイド|再現性・評価・LTまで

ポートフォリオって「作ったものの置き場」でしょ? いいえ。採用側が見たいのは「意思決定に効いた証拠」と「再現性」です。 本ガイドは、未経験〜初学者が週10時間×4〜6週で、テーマ選定→要件定義→データ ...

最近のコメント

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

    ふみと

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

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

    -Python基礎