
「昨日は動いてたのに、今日は壊れた…」
データ分析や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。仮想環境は[内部リンク:Jupyter Notebookの基本]、Git運用は[内部リンク:Git/GitHub入門]を参照。
ステップ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
ステップ1:pytestインストール&最小実行
pip install pytest pytest-cov pandas
pytest -q # テスト探索→実行
ルール:tests/
配下で、ファイルはtest_*.py
/*_test.py
、関数はtest_*
で始めます。
ステップ2:AAA(Arrange-Act-Assert)の型
import pandas as pd
def add\_amount(df: pd.DataFrame) -> pd.DataFrame:
assert {"qty","price"}.issubset(df.columns)
return df.assign(amount=df\["qty"] \* df\["price"]) # 純関数
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)
処理は副作用なしの純関数に寄せ、DataFrameはpandas.testing
で比較します。
ステップ3:fixtureで共通準備を使い回す
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
conftest.py
に集約し、tmp_path
で一時DB/ファイルを安全に扱います。
ステップ4:parametrizeで条件網羅を短く
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
境界値(0や最小/最大)をデータ駆動でカバーしましょう。
ステップ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() # 必要に応じて欠損も確認
ステップ6:SQLite I/Oのテスト(副作用ゼロ)
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)
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
やインメモリで副作用ゼロに。
ステップ7:外部依存の除去(依存注入/monkeypatch)
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
from pathlib import Path
def test\_save\_text\_writes\_to\_tmp(tmp\_path):
from src.report import save\_text
p = tmp\_path / "reports/hello.txt"
got = save\_text(p, "hello")
assert got.exists() and got.read\_text() == "hello"
ファイルパス/HTTP/現在時刻などは引数で受け取る(依存注入)。必要ならmonkeypatch
で差し替えます。
ステップ8:乱数・現在時刻の固定(再現性)
前述のconftest.py
のfixed_seed
をautouse=True
で全テストに適用。
ステップ9:可視化のテスト(画像の“生成”を担保)
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")
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
画像の“中身”は比較せず、生成されたこととサイズ>0を確認する回帰テストにします。
ステップ10:MLの再現性テスト(CV±std/閾値)
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連携)
pyproject.toml(pytest設定例|コピペ可)
[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-missing
CI連携は[内部リンク:Git/GitHub入門]のActionsテンプレを流用してください。
レビュー込みで“壊れないポートフォリオ”へ最短到達
テストを入れると、在宅×副業の毎月レポートやML実験の事故率が激減。設計→テスト→CIまで短期間で整えるなら、質問対応とレビューのあるスクールが近道です。
・株式会社キカガク:実務再現型の課題設計とレビュー。転職直結に◎。
・Tech Academy:質問の速さ×短時間運用で継続しやすい。副業/在宅に◎。
TechAcademy データサイエンスコース(受講料:174,600円~ ※更に割引あり)

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

用途別の重点ポイント(3タイプ)
- 社会人(転職):ML再現性テスト+CV±stdを最優先。READMEに実行コマンドとCIバッジ。→ [内部リンク:ポートフォリオ完全ガイド]
- 副業(稼ぎたい):SQLite I/O+Excel出力の回帰テストを整備。タグ/リリースで版管理。→ [内部リンク:データレポート納品の型] [内部リンク:SQLite×Python]
- 主婦/夫(在宅):
tmp_path/monkeypatch
で副作用ゼロのテストから。15分で1テストの積み上げ。→ [内部リンク:在宅×Python:子育てと両立する1日1時間学習術]
ミニプロジェクト(提出推奨)
課題:「月次レポート」のfeatures.py
/db.py
/plot.py
に対し、10本のpytestを作成。CI(Actions)で自動実行し、READMEに手順とバッジを追加。
チェックリスト(コピペ可)
- AAAで整理されたテスト
- parametrizeで境界値を網羅
tmp_path
で一時ファイル/DBを使用- monkeypatch/依存注入で外部依存を除去
pandas.testing
でDataFrame比較- MLの下限/分散テスト(必要な場合)
- CIが緑(main保護)
付録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)
この記事から次に読むべきもの(内部リンク)
-
-
【保存版】Git/GitHub入門:バージョン管理・ブランチ戦略・レビュー・自動化を“実務の型”で最短習得
「分析やノートブックは作れるけど、壊れない運用の“型”がない…」 「final_v3_fix2_LAST.xlsx地獄から抜け出して、レビューと自動化まで一気通貫で回したい!」 この記事では、未経験〜 ...
-
-
【保存版】SQLite×Pythonで作る“ローカルDWH”——ETL・集計・レポート自動化の最短手順
ローカルでゼロ構築、ファイル1つで完結、サーバ不要。本記事はSQLite×Pythonで“毎日回る”ETL・集計・レポート自動化を最短で作るための完全ガイドです。データ設計→DB作成→ETL(取り込み ...
-
-
【保存版】データレポート納品の型:要件定義→ETL→検証→可視化→Excel/PDF→引き継ぎまで、失注しないワークフロー完全版
“いい分析”より“伝わる納品”。副業や実務で評価されるのは、意思決定に効く1枚と再現できるパッケージを期限通り出せること。 本記事は、未経験〜初学者が週10時間×2〜3週で、要件定義→データ受領→ET ...
-
-
【保存版】scikit-learn基礎:回帰・分類・前処理・パイプライン・交差検証を“実務の型”で習得
機械学習で迷子になる最大の理由は、前処理→学習→評価→改善の順番が曖昧なまま個々のアルゴリズムに飛びつくこと。本記事は、未経験〜初学者が週10時間×2〜3週で到達できるscikit-learnの最短ル ...
-
-
【保存版】モデル評価:指標の選び方・交差検証・閾値最適化・ビジネス接続を“実務の型”で解説
精度が上がらない原因の多くは「評価設計の誤り」にあります。評価とは「何点取れたか」ではなく、意思決定に耐えるかを測る営み。この記事では、回帰/分類/ランキングの指標の選び方、交差検証の正しい使い分け、 ...
最近のコメント