AIの業務活用

小さなデータで AI 生成コードを検証する

手計算できる4行のデータと12個の unit tests を使って aggregation function を確認します。通常値だけでなく、空入力、0、小数、不正入力も検証します。

目次を表示

この翻訳はAIで作成しました。コード、単位、数値は原文と併せて確認してください。各言語のネイティブ話者による校閲は、まだ完了していません。 English

対象読者AI が生成した Python コードが動作するという理由だけで業務に使うことに不安がある初学者

準備するもの
  • Python 3.12 以降をインストールし、terminal で python --version を使って version を確認します。
  • example ZIP を新しいフォルダへ展開します。ZIP archive 内から直接 example を実行しないでください。
  • example.py があるフォルダで terminal を開きます。Windows で python command が使えない場合は py を使用します。macOS または Linux では、環境によって python3 を使用します。
  • 外部 packages や accounts は不要です。付属ファイルはこの tutorial 用に作成した合成データです。

01実行する前に答えを決める

AI のコードが error なく終了したからといって、計算が正しいとは限りません。この記事では実際の AI service には接続せず、独立して作成した aggregation function と小さな合成データを使って検証手順を練習します。同じ方法を、確認したい function に適用できます。API key や外部 packages は不要です。

  1. input.csv の4行を読み、quantity × unit price を自分で計算します。
  2. expected.json を開き、4 rows、quantity 6、amount 11101.50 と記載されていることを確認します。
  3. 以下の command を実行します。最初に12個の unit tests が実行され、その後で CSV aggregation と expected values との比較が行われます。
  4. すべての比較に合格した場合だけ作成される outputs/summary.json と verification.txt を確認します。
bash
python example.py

024行分の計算を表で比較する

itemquantityunit_price行ごとの金額
노트22500.005000.00
31200.503601.50
노트12500.002500.00
Sample0999.990.00

rows は4件ですが、total quantity は 2 + 3 + 1 + 0 = 6 です。total amount は 5000.00 + 3601.50 + 2500.00 = 11101.50 です。最後の quantity 0 の行も input rows の件数には含まれます。item ごとでは、노트 (notebook) は quantity 3、amount 7500.00、펜 (pen) は quantity 3、amount 3601.50、샘플 (sample) は quantity 0、amount 0.00 です。

したがって total だけを比較せず、row count、total quantity、per-item values を一緒に確認してください。notebook のように同じ item を2行に分けて入力すると、grouping と summing も確認できます。per-item results の順序は name で sort されており、sales rank や input order を意味しません。

03function が受け付ける入力ルールを書く

項目許可ルール拒否例
Column3つ: item, quantity, unit_pricecolumns の欠落または追加
item先頭と末尾の spaces を除去した後の空でない string空文字列または whitespace only
quantity0 から 1,000,000 までの integer string-1, 1.5, 1e2
unit_price0 から 1,000,000,000、小数点以下2桁までNaN, Infinity, 0.001, 1,000

amounts は同じ架空通貨を使用すると仮定します。negative quantities と unit prices は許可しないため、returns の表現ルールはありません。先に input rules を定義しておくと、コードが値を黙って 0 に変更したり、一部の rows を skip したりしていないか判断できます。この function は invalid row を見つけると ValueError で停止します。

04aggregation と tests を含む完全なコード

aggregate_sales は input rows を変更せず、result だけを返します。file の読み込みと result の書き込みは main に分離しています。これにより tests では small dictionaries を直接渡し、calculation だけを確認できます。各 expected value は assertEqual で比較し、拒否されるべき values は assertRaises で確認します。

example.py
"""집계 함수를 손계산 기대값과 unittest로 검증합니다. 외부 AI 호출은 없습니다."""

import argparse
import csv
import json
import re
import sys
import unittest
from decimal import Decimal
from pathlib import Path

BASE = Path(__file__).resolve().parent
OUTPUT = BASE / "outputs"
FIELDS = {"item", "quantity", "unit_price"}


def money(cents):
    return f"{cents // 100}.{cents % 100:02d}"


def aggregate_sales(rows):
    """문자열 딕셔너리 행을 검증하고 집계합니다. rows와 원본 파일을 변경하지 않습니다."""
    totals = {}
    row_count = total_quantity = total_cents = 0
    for number, row in enumerate(rows, start=1):
        if not isinstance(row, dict) or set(row) != FIELDS:
            raise ValueError(f"{number}행: item,quantity,unit_price 세 열이 필요합니다.")
        if any(not isinstance(value, str) for value in row.values()):
            raise ValueError(f"{number}행: 모든 값은 문자열이어야 합니다.")
        item, quantity_text, price_text = (row[key].strip() for key in ("item", "quantity", "unit_price"))
        if not item:
            raise ValueError(f"{number}행: 품목이 비어 있습니다.")
        if not re.fullmatch(r"[0-9]{1,7}", quantity_text):
            raise ValueError(f"{number}행: 수량은 0 이상의 정수여야 합니다.")
        quantity = int(quantity_text)
        if quantity > 1_000_000:
            raise ValueError(f"{number}행: 수량은 1,000,000 이하여야 합니다.")
        if not re.fullmatch(r"[0-9]{1,10}(?:\.[0-9]{1,2})?", price_text):
            raise ValueError(f"{number}행: 단가는 0 이상, 소수점 둘째 자리까지 입력하세요.")
        price = Decimal(price_text)  # 문자열에서 직접 생성해 float 오차를 피합니다.
        if price > Decimal("1000000000"):
            raise ValueError(f"{number}행: 단가는 1,000,000,000 이하여야 합니다.")
        cents = int(price * 100) * quantity
        bucket = totals.setdefault(item, {"quantity": 0, "cents": 0})
        bucket["quantity"] += quantity
        bucket["cents"] += cents
        total_quantity += quantity
        total_cents += cents
        row_count += 1
    return {
        "row_count": row_count,
        "quantity": total_quantity,
        "amount": money(total_cents),
        "by_item": [
            {"item": item, "quantity": totals[item]["quantity"], "amount": money(totals[item]["cents"])}
            for item in sorted(totals)
        ],
    }


def sale(item="펜", quantity="1", unit_price="1200.50"):
    return {"item": item, "quantity": quantity, "unit_price": unit_price}


class TestAggregate(unittest.TestCase):
    # 기대값은 검증 대상 함수로 만들지 않고 손계산한 상수로 적습니다.
    def test_empty_input(self):
        self.assertEqual(aggregate_sales([]), {"row_count": 0, "quantity": 0, "amount": "0.00", "by_item": []})

    def test_single_row(self):
        self.assertEqual(aggregate_sales([sale(quantity="3")])["amount"], "3601.50")

    def test_zero_quantity(self):
        result = aggregate_sales([sale(quantity="0")])
        self.assertEqual((result["row_count"], result["quantity"], result["amount"]), (1, 0, "0.00"))

    def test_repeated_item(self):
        result = aggregate_sales([sale(quantity="2"), sale(quantity="1")])
        self.assertEqual(result["by_item"], [{"item": "펜", "quantity": 3, "amount": "3601.50"}])

    def test_decimal_addition(self):
        result = aggregate_sales([sale(unit_price="0.10"), sale(unit_price="0.20")])
        self.assertEqual(result["amount"], "0.30")

    def test_whitespace(self):
        result = aggregate_sales([sale(item=" 펜 ", quantity=" 1 ", unit_price=" 2.00 ")])
        self.assertEqual(result["by_item"], [{"item": "펜", "quantity": 1, "amount": "2.00"}])

    def test_bad_quantity(self):
        for value in ("-1", "1.5", "", "1000001", "1e2", "1"):
            with self.subTest(value=value), self.assertRaises(ValueError):
                aggregate_sales([sale(quantity=value)])

    def test_bad_price(self):
        for value in ("-1", "0.001", "NaN", "Infinity", "", "1,000", "1000000001"):
            with self.subTest(value=value), self.assertRaises(ValueError):
                aggregate_sales([sale(unit_price=value)])

    def test_blank_item(self):
        with self.assertRaises(ValueError):
            aggregate_sales([sale(item=" ")])

    def test_wrong_columns(self):
        for row in ({"item": "펜"}, {**sale(), "extra": "x"}):
            with self.subTest(row=row), self.assertRaises(ValueError):
                aggregate_sales([row])

    def test_non_string(self):
        with self.assertRaises(ValueError):
            aggregate_sales([sale(quantity=True)])

    def test_upper_boundary(self):
        self.assertEqual(
            aggregate_sales([sale(quantity="1000000", unit_price="1000000000")])["amount"],
            "1000000000000000.00",
        )


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--test", action="store_true", help="단위 테스트만 실행합니다.")
    args = parser.parse_args()
    # unittest의 명시적 비교는 python -O에서도 생략되지 않습니다.
    result = unittest.TextTestRunner(verbosity=2, stream=sys.stdout).run(
        unittest.defaultTestLoader.loadTestsFromTestCase(TestAggregate)
    )
    if not result.wasSuccessful():
        return 1
    if args.test:
        return 0
    if OUTPUT.exists() or OUTPUT.is_symlink():
        raise FileExistsError("outputs가 이미 있습니다. 기존 결과를 옮긴 뒤 실행하세요.")
    with (BASE / "input.csv").open("r", encoding="utf-8-sig", newline="") as stream:
        reader = csv.DictReader(stream, strict=True)
        if reader.fieldnames != ["item", "quantity", "unit_price"]:
            raise ValueError("input.csv의 열은 item,quantity,unit_price 순서여야 합니다.")
        actual = aggregate_sales(reader)
    expected = json.loads((BASE / "expected.json").read_text(encoding="utf-8"))
    if actual != expected:
        raise ValueError("예제 집계가 expected.json의 손계산 기대값과 다릅니다.")
    OUTPUT.mkdir()
    with (OUTPUT / "summary.json").open("x", encoding="utf-8") as stream:
        json.dump(actual, stream, ensure_ascii=False, indent=2)
        stream.write("\n")
    with (OUTPUT / "verification.txt").open("x", encoding="utf-8") as stream:
        stream.write(f"unittest: {result.testsRun}개 통과\n예제 기대값: 일치\n외부 AI API 호출: 없음\n")
    print(f"완료: {actual['row_count']}행, 수량 {actual['quantity']}, 금액 {actual['amount']}")
    print(OUTPUT)
    return 0


if __name__ == "__main__":
    try:
        raise SystemExit(main())
    except (OSError, ValueError, csv.Error) as error:
        print(f"중지: {error}", file=sys.stderr)
        raise SystemExit(2)

unit price は string から Decimal に変換し、100 を掛けて integer units にした後で合計します。結果の amount も小数点以下2桁の string として返します。これはこの例の two-decimal unit price rule に合う選択であり、すべての数値計算に共通する universal rule ではありません。

05tests が確認する通常・境界・エラーケース

カテゴリ組み込み tests確認内容
Normal1 row, repeated item, leading and trailing spaces基本計算と grouping
BoundaryEmpty input, quantity 0, upper limit小さい値と大きい値での定義
Decimals0.10 + 0.20 = 0.30amount representation と addition
ErrorInvalid quantity, unit price, blank item, columns, typesinvalid values が黙って受け入れられないこと

各 test name の横に ok が表示され、最後に ‘Ran 12 tests’ と ‘OK’ が出ることを確認します。さらに ‘완료: 4행, 수량 6, 금액 11101.50’ (Done: 4 rows, quantity 6, amount 11101.50) が表示された場合だけ、CSV expected-value comparison も合格しています。unit tests だけが合格して result が expected.json と異なる場合、default run は失敗し、新しい results は保存されません。

bash
python example.py --test

--test は unit tests だけを再実行し、outputs は作成しません。results folder がすでに存在していても使用できるため、function を編集しながら素早く確認するのに適しています。一方、default command は既存 outputs を上書きせず停止します。

06一度だけ failing test を作ってみる

  1. 新しく展開した folder に example.py の copy を用意します。元ファイルは戻せるよう保持します。
  2. calculation line の int(price * 100) * quantity から最後の * quantity だけを削除し、quantity を無視する error を作ります。
  3. python example.py --test を実行し、one-row quantity 3 や repeated-item などの tests が失敗することを確認します。
  4. 元の calculation line に戻し、--test を再実行して12個すべてが合格することを確認します。

この実験の目的は、tests が error を検出できることを確認することです。この例の作成時にも、同じ種類の error を temporary copy に意図的に入れ、tests が失敗し results が書き込まれないことを確認しました。意味のある failure を一度見ることで、test code は動いているのに何も確認していない状態を避けやすくなります。

07AI から得た function に適用する手順

新しいコードでは、まず input と output の例を固定します。original files に書き込む部分がある場合は copies に限定し、先に calculation を行う function を分離します。normal data、empty data、0、repeated items、許可されない values を1セットずつ用意し、expected values を自分で計算します。

  1. function の business rules を文章で書きます。たとえば returns を拒否するのか、negative amounts として計算するのかを決めます。
  2. 各 rule について small input と expected value を作ります。overall total と per-item results を一緒に比較します。
  3. 同じ input を review 対象 function に入れ、results の differences を確認します。failure を消すために expected values を先に変更しないでください。
  4. revised code で既存 tests を再実行した後、新たに見つけた error cases も tests に残します。

input.csv を自分の data に置き換える場合は、expected.json も別途計算して更新する必要があります。input だけを変更した後、expected values と不一致だからといって code defect だと結論付けないでください。どの rules とどの data が変わったかを確認するところから reproducible verification が始まります。

08failure message を読み、原因を絞り込む

結果意味次に確認すること
FAIL or FAILEDcalculated result が test の expected value と異なるfailed test の name と2つの values を確認します。
Stopped due to input rulesCSV value が allowed range の外にある示された row と original data を確認します。
Differs from the hand-calculated expected valueCSV aggregation が expected.json と一致しないdata が変わったか、hand calculation が正しいかの両方を再確認します。
outputs already exists以前の successful result が存在する--test だけで checks を実行するか、results を別の場所に移して保持します。

この例で tests に合格することは、記載した rules に対する evidence です。実際の AI model の performance 測定でも、すべての accounting work の accuracy 保証でもありません。taxes, discounts, exchange rates, returns, multiple currencies, large data での performance には別の rules と tests が必要です。failure cases を増やすほど、適用可能な範囲をより明確に説明できます。

実行・検証の記録

2026-09-19 · Windows 11 · CPython 3.12.14 · 追加 packages なし · distribution の temporary copy で実行

  • 組み込み unittest 12件が合格
  • 合成 CSV 4 rows が手計算した expected value 11101.50 と一致
  • input と expected values が一致しない場合、results 作成前に停止
  • tests が quantity を無視する意図的な calculation error を検出
  • --test の繰り返し実行と original SHA-256 の保持を確認
検証範囲の限界
  • 実際の AI API は呼び出していません。
  • tax, exchange rate, returns, performance verification は含まれていません。
  • Windows CPython 3.12.14 での実行結果です。

サイト全体の執筆・検証方針

自分で実行するためのサンプルファイル

コード、入力データ、実行手順が含まれています。展開して、まずREADME.txtを読んでください。

サンプルZIPをダウンロード

サンプルコード、ファイル名、入力キーは原文のままです。翻訳本文のコマンドと確認手順も併せて参照してください。

独自に作成した練習用資料 · 元のファイルを別に保管してから実行してください。

参考資料

説明と例は独自に作成しました。関連する動作や概念は、以下の公式資料で確認できます。