Python基礎

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

ローカルでゼロ構築、ファイル1つで完結、サーバ不要。

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

この記事で身に付く力

  • ローカル完結の小さなDWH設計:ファイル1つ(.sqlite)にクエリと再現性を閉じ込める

  • 実務で効く高速化の要点:PRAGMA/インデックス/バルク挿入の使いどころ

  • 配布までの一気通貫:pandas + ExcelWriter + 図の出力で“毎月回る”納品パッケージ

現場メモ

Excelブック群の手作業更新は“人時”が溶けがち。SQLiteに集約→SQLで整形→ExcelWriter配布に切り替えると、**更新は数分&再現性100%**を目指せます。

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

CSV のまま複雑 JOIN で破綻 → SQLite に入れてから SQL で整形

Notebook に前処理コピペが散乱 → SQL を関数化・ファイル分割で再利用

毎月の Excel 納品でコピペ地獄 → ExcelWriter で自動出力

結論: “軽量DWH=SQLite”を採用し、Python を ETLとレポート配布の自動化エンジンにします。

使用環境

Python 3.10+ / pandas 2.x / SQLite3(標準ライブラリ) を想定。Windows/Mac/Linuxいずれでも動作します。
初学者メモ:外部サーバは不要。.sqlite ファイル1つでDBが成立します。

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

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

狙い: データ(data/)・SQL(src/sql/)・出力(reports/)を分け、再現手順を README に固定します。

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

やること: 先に表の形(列・型・主キー・制約)を決め、重複と不整合をDBで防ぐ

例:sales(order_date, store, product, qty, price)stores(store, area, open_date)

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);

ポイント

  • UNIQUE(...)重複挿入を物理的にブロック
  • 複合インデックスはよく絞る列→範囲列の順で

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

目的: DBファイルを作成し、PRAGMAで挙動を調整、CREATE文を一気に実行します。
なぜ? WALは読み取りの並行性、foreign_keys親子関係の破損防止に効きます。

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)とバリデーション

目的: "壊れた行" をDBに入れない。取り込み前に型・欠損・範囲を確認します。

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のプレースホルダ)

目的: SQLインジェクションを避け、トランザクションでまとめて安全に書き込みます。

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()

ポイント: VALUES(?,?,?)**?プレースホルダ**を使うのが安全の基本です。

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

考え方: 重い前処理は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()

読み方: WITH monthly AS (...)CTE。段階的に集計を組み立てると、可読性と再利用性が上がります。

レポート出力(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()

現場Tips: 入出力の境目にアサーションを置くと、異常時に早期に落ちて気づけます。

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

  • 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()

要点: with con:トランザクション境界が品質の土台になります。

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

  • ローカル共有:.sqlite を OneDrive/Google Drive で共有(同時編集ロックに注意)
  • 行数が増えたら:抽出→Parquet 併用で軽量化
  • 将来の移植:同じ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週で、SEL ...

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

基礎文法の次は、実務で毎回出る処理を“型”として覚える段階です。 本記事は、pandas 2.x を前提に、欠損・外れ値・結合・ウィンドウ関数・時系列・カテゴリ処理・集計の自動化・大規模データの分割処 ...

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

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

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

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

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

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

最近のコメント

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

    ふみと

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

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

    -Python基礎