
「昨日は動いてたのに、今日は壊れた…」
データ分析やETL、機械学習のコードで多発するこの悲劇。実は“テスト不在”が9割です。
本記事は、pytestで“壊れないPython”を作るための実務ガイドです。
未経験〜初学者でも1〜2週間で、基本→AAA→fixture/parametrize→pandas/SQL→モック/monkeypatch→乱数固定→可視化→ML再現性→カバレッジ/CIまでを一気通貫で整えられるよう、コピペOKのテンプレ+チェックリストで解説します。
この記事で身に付く力
- pytestの基本〜実務に効くテスト設計を一気通貫で理解
- pandas/SQLite/可視化/MLのテスト“型”をコピペで適用
- CIとカバレッジで「昨日も今日も動く」状態を維持
なぜデータ系Pythonは“壊れやすい”のか
分析・ETL・MLの現場では、列の追加/削除や外部環境の変化が日常茶飯事。
暗黙知に依存していると、小さな変更が連鎖的な崩壊を引き起こします。
- 暗黙の前提:列名/型/粒度がドキュメント化されず、ある日列が消えて全滅。
- 外部依存:ファイル/DB/API/現在時刻/乱数に引っ張られて挙動がブレる。
- ノートブック依存:セル順・手動実行で再現不能。
解決の基本方針
-
処理を関数化(副作用は外に出す)
-
pytestで最小のテストから積み上げ
-
CIで毎回自動実行(壊れたら即わかる)
以降のテンプレをそのまま採用してください。
現場で生き残った“壊れない進め方”
筆者(ふみと)は大手企業でデータ/マーケティングサイエンティストを10年。
ノートブック単体運用で痛い目を何度も見ました。
最終的に行き着いたのが、src配下へ関数化→pytestで即時検証→CIの三点セット。
これで在宅や副業でも納期と品質を安定させられます。
pytest 実務の型:12ステップ(コピペOK)
前提環境:Python 3.10+ / pytest 7+ / pandas 2.x / SQLite
ステップ0:プロジェクト雛形
project/
├─ src/
│ ├─ features.py # 前処理/特徴量
│ ├─ report.py # Excel/図の出力
│ └─ db.py # SQLite I/O
├─ tests/
│ ├─ test_features.py
│ ├─ test_db.py
│ └─ conftest.py # 共有fixture
├─ data/ (raw/warehouse) # 非追跡推奨
├─ reports/ # 出力(非追跡推奨)
├─ requirements.txt
├─ pyproject.toml # pytest設定
└─ README.mdポイント:src/に業務ロジック、tests/にテスト。データやレポートの生成物はGitに載せないのが安全です。
ステップ1:pytestを入れて最小実行
やりたいこと:テストを1本でも動かして“痛みなく回る”体験を得る。
pip install pytest pytest-cov pandas
pytest -q # テスト探索→実行命名ルール
関数:test_*
ファイル:tests/test_*.py または *_test.py
ステップ2:AAA(Arrange–Act–Assert)で“読みやすい”型にする
やりたいこと:前処理関数add_amountがqty*priceを正しく計算するか検証。
# src/features.py
import pandas as pd
def add_amount(df: pd.DataFrame) -> pd.DataFrame:
# 失敗時の原因がすぐ分かるよう、前提条件を明示
assert {"qty", "price"}.issubset(df.columns)
# 副作用なし(受け取ったDFを壊さない)
return df.assign(amount=df["qty"] * df["price"])
# tests/test_features.py
import pandas as pd
from pandas.testing import assert_frame_equal
from src.features import add_amount
def test_add_amount_multiplies_qty_and_price():
# Arrange(入力準備)
df = pd.DataFrame({"qty": [2, 3], "price": [500, 1200]})
# Act(関数を呼ぶ)
got = add_amount(df)
# Assert(期待と比較)
expect = pd.DataFrame({
"qty": [2, 3], "price": [500, 1200], "amount": [1000, 3600]
})
assert_frame_equal(got, expect)初心者向けTip:DataFrameは==で比較せず、pandas.testingの比較関数で列順・型・インデックスの違いも検出します。
ステップ3:fixtureで“毎回の準備”を共通化
やりたいこと:テストでよく使うサンプルDFや一時SQLite接続、乱数固定を共通化。
# tests/conftest.py
import pandas as pd
import sqlite3
import pytest
import random, numpy as np
@pytest.fixture
def sample_df():
return pd.DataFrame({
"order_date": ["2024-04-01"], "store": ["S01"], "qty": [2], "price": [500]
})
@pytest.fixture
def sqlite_conn(tmp_path: pytest.TempPathFactory):
db = tmp_path / "test.sqlite"
con = sqlite3.connect(db)
yield con
con.close()
# すべてのテストで乱数を固定(再現性の土台)
@pytest.fixture(autouse=True)
def fixed_seed():
random.seed(42); np.random.seed(42)
yieldポイント:tmp_pathはテストごとにクリーンな一時ディレクトリを提供。副作用の混入を防げます。
ステップ4:@pytest.mark.parametrizeで条件網羅を短く
やりたいこと:qtyやpriceの境界値(0など)をデータ駆動で網羅。
import pytest
import pandas as pd
from src.features import add_amount
@pytest.mark.parametrize("qty, price, amount", \[
(2, 500, 1000),
(0, 500, 0),
(5, 0, 0),
])
def test\_add\_amount\_param(qty, price, amount):
df = pd.DataFrame({"qty":\[qty], "price":\[price]})
got = add\_amount(df)
assert int(got.loc\[0, "amount"]) == amountポイント:境界値テストを“表”で書ける。テストケースの追加・削除も簡単。
ステップ5:pandasの列・型・欠損を検証
やりたいこと:想定列が揃い、型が妥当で、欠損が出ていないかを確認。
from pandas.testing import assert_series_equal
from src.features import add_amount
def test\_columns\_and\_dtypes(sample\_df):
got = add\_amount(sample\_df)
assert set(\["order\_date","store","qty","price","amount"]).issubset(got.columns)
assert got\["qty"].dtype.kind in "iu" # int/uint
assert got\["price"].dtype.kind in "if" # int/float
assert not got.isna().any().any() # 必要に応じて欠損も確認ポイント:型の破壊(int→floatなど)はバグの温床。dtype.kindでざっくり守るのが簡単です。
ステップ6:SQLite I/Oを“副作用ゼロ”で回帰テスト
やりたいこと:DB書き込み→集計読み出しの往復が壊れていないか。
# src/db.py
import sqlite3, pandas as pd
def write_sales(con: sqlite3.Connection, df: pd.DataFrame) -> None:
df.to_sql("sales", con, if_exists="append", index=False)
def read_monthly(con: sqlite3.Connection) -> pd.DataFrame:
q = """
WITH m AS (
SELECT strftime('%Y-%m', order_date) AS ym, store,
SUM(qty*price) AS sales
FROM sales GROUP BY ym, store
) SELECT * FROM m
"""
return pd.read_sql_query(q, con)# tests/test_db.py
import pandas as pd
from src.db import write_sales, read_monthly
def test_sqlite_roundtrip(sqlite_conn):
df = pd.DataFrame({
"order_date": ["2024-04-01", "2024-04-02"],
"store": ["S01", "S01"],
"qty": [2, 3], "price": [500, 1200],
})
write_sales(sqlite_conn, df)
got = read_monthly(sqlite_conn)
assert set(["ym", "store", "sales"]).issubset(got.columns)
assert float(got.loc[0, "sales"]) > 0ポイント:本番DBに触らず、tmp_pathやインメモリSQLiteで“安全に再現”。
ステップ7:外部依存の除去(依存注入/monkeypatch)
やりたいこと:ファイルパス・現在時刻・HTTPなど外部に依存する値を引数で受け取る(=依存注入)。
# src/report.py
from pathlib import Path
def save_text(path: Path, content: str) -> Path:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(content, encoding="utf-8")
return path# tests/test_report.py
from pathlib import Path
from src.report import save_text
def test_save_text_writes_to_tmp(tmp_path):
p = tmp_path / "reports/hello.txt"
got = save_text(p, "hello")
assert got.exists() and got.read_text() == "hello"ポイント:必要に応じてmonkeypatchで環境依存の関数・変数を差し替えれば、どの環境でも同じ結果に。
ステップ8:乱数・現在時刻の固定(再現性)
やりたいこと:モデル学習やサンプリングの再現性を保つ。
すでにconftest.pyで乱数は固定済み。現在時刻はfreezegun等の利用も可。
ステップ9:可視化は“生成されたこと”をテスト
やりたいこと:画像の中身ではなく生成成功(ファイルが存在・サイズ>0)を回帰テスト。
# src/plot.py
import matplotlib.pyplot as plt
def plot_trend(df, out):
plt.figure()
plt.plot(df["x"], df["y"], marker="o")
plt.tight_layout()
plt.savefig(out, dpi=120, bbox_inches="tight")# tests/test_plot.py
import pandas as pd
from src.plot import plot_trend
def test_plot_trend_saves_png(tmp_path):
df = pd.DataFrame({"x": ["2024-04", "2024-05"], "y": [10, 12]})
out = tmp_path / "trend.png"
plot_trend(df, out)
assert out.exists() and out.stat().st_size > 0ステップ10:MLの再現性(CVの平均と分散に下限/上限)
やりたいこと:交差検証の平均スコアが下限以上、分散が上限以下で、退行を検知。
import numpy as np
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import StratifiedKFold, cross_val_score
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
def test\_cv\_auc\_is\_reasonable():
Xy = load\_breast\_cancer()
X, y = Xy.data, Xy.target
pipe = Pipeline(\[
("sc", StandardScaler()),
("lr", LogisticRegression(max\_iter=1000, random\_state=42, n\_jobs=-1))
])
cv = StratifiedKFold(n\_splits=5, shuffle=True, random\_state=42)
scores = cross\_val\_score(pipe, X, y, scoring="roc\_auc", cv=cv, n\_jobs=-1)
assert np.mean(scores) > 0.98 # 下限
assert np.std(scores) < 0.02 # ばらつき上限
ポイント:random_stateを固定し、CV設計(層化・シャッフル)でブレを制御します。
ステップ11:マーク/スキップ/xfailの活用
やりたいこと:環境依存や“将来直す”テストを適切に管理。
import sys, pytest
@pytest.mark.skipif(sys.platform == "win32", reason="OS依存のためスキップ")
def test\_posix\_only():
assert True
@pytest.mark.xfail(reason="将来対応予定の仕様")
def test\_future\_behavior():
assert 1/0 == 0ステップ12:設定・実行・カバレッジ(CI連携)
やりたいこと:pytestの既定値を整え、CIで常時起動。必要ならカバレッジも確認。
# pyproject.toml(一例)
[tool.pytest.ini_options]
minversion = "7.0"
addopts = "-q --maxfail=1 --disable-warnings"
testpaths = ["tests"]
filterwarnings = ["ignore::DeprecationWarning"]# カバレッジ(任意)
pip install pytest-cov
pytest --cov=src --cov-report=term-missingCI:GitHub Actionsのテンプレを使い、push/pull_requestで自動実行。mainに保護ルールを掛ければ“壊れたコードが入らない”状態になります。
レビュー込みで“壊れないポートフォリオ”へ最短到達
テストを入れると、在宅×副業の毎月レポートやML実験の事故率が激減。設計→テスト→CIまで短期間で整えるなら、質問対応とレビューのあるスクールが近道です。
・株式会社キカガク:実務再現型の課題設計とレビュー。転職直結に◎。
・Tech Academy:質問の速さ×短時間運用で継続しやすい。副業/在宅に◎。
TechAcademy データサイエンスコース(受講料:174,600円~ ※更に割引あり)

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

用途別の重点ポイント(3タイプ)
- 社会人(転職):ML再現性テスト(平均/分散)を最優先。READMEに実行コマンドとCIバッジ。
- 副業(稼ぎたい):SQLite I/O+Excel出力の回帰テスト。タグ/リリースで版管理。
- 主婦/夫(在宅):
tmp_path/monkeypatchで副作用ゼロの小さなテストから。“1日15分×1テスト”でも十分積み上がります。
付録A:よく使うアサーション早見表
assert 1 == 1
import math; assert math.isclose(0.1+0.2, 0.3, rel_tol=1e-9)
from pandas.testing import assert_frame_equal, assert_series_equal付録B:エラーパス(失敗経路)のテスト
import pytest
from src.features import add_amount
import pandas as pd
def test\_add\_amount\_raises\_when\_columns\_missing():
with pytest.raises(AssertionError):
add\_amount(pd.DataFrame({"qty":\[1]}))付録C:pytest実行の小技
pytest -k amount # 名前にamountを含むテストだけ
pytest tests/test_db.py::test_sqlite_roundtrip -q
pytest -x # 最初の失敗で停止
pytest -n auto # 並列実行(pytest-xdist)まとめ
- 関数化→pytest→CIの三点で“壊れないPython”。
- pandas/SQLite/可視化/MLそれぞれにテストの型がある。
- テストは短く・自動で・毎回回す。小さく始めて継続が正義。
この記事から次に読むべきもの(内部リンク)
-
-
【保存版】Git/GitHub入門:バージョン管理・ブランチ戦略・レビュー・自動化を“実務の型”で最短習得
「分析やノートブックは作れるけど、壊れない運用の“型”がない…」 「final_v3_fix2_LAST.xlsx地獄から抜け出して、レビューと自動化まで一気通貫で回したい!」 この記事では、未経験〜 ...
-
-
【保存版】SQLite×Pythonで作る“ローカルDWH”|ETL・集計・レポート自動化の最短手順
ローカルでゼロ構築、ファイル1つで完結、サーバ不要。 本記事はSQLite×Pythonで“毎日回る”ETL・集計・レポート自動化を最短で作るための完全ガイドです。データ設計→DB作成→ETL(取り込 ...
-
-
【保存版】データレポート納品の型:要件定義→ETL→検証→可視化→Excel/PDF→引き継ぎまで、失注しないワークフロー完全版
“いい分析”より“伝わる納品”。副業や実務で評価されるのは、意思決定に効く1枚と再現できるパッケージを期限通り出せること。 本記事は、未経験〜初学者が 週10時間×2〜3週 で、要件定義 → データ受 ...
-
-
【保存版】scikit-learn基礎:回帰・分類・前処理・パイプライン・交差検証を“実務の型”で習得
機械学習で迷子になる最大の理由は、前処理→学習→評価→改善の順番が曖昧なまま個々のアルゴリズムに飛びつくこと。 本記事は、未経験〜初学者が週10時間×2〜3週で到達できるscikit-learnの最短 ...
-
-
【保存版】モデル評価:指標の選び方・交差検証・閾値最適化・ビジネス接続を“実務の型”で解説
精度が上がらない原因の多くは「評価設計の誤り」にあります。 評価は「何点取れたか」を競うものではなく、意思決定に耐えうるか を検証する営みです。本記事は、回帰/分類/ランキングの 指標選定 → 交差検 ...
最近のコメント