
ローカルでゼロ構築、ファイル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/Linux:
crontab -e→0 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円~ ※更に割引あり)

株式会社キカガク 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: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週で、テーマ選定→要件定義→データ ...
最近のコメント