Pythonのdecimalモジュール完全ガイド|浮動小数点誤差を防ぐ方法と使い方の実例

python

Pythonで計算していて、こんな不思議な結果に出会ったことはありませんか?

print(0.1 + 0.2)
# 結果: 0.30000000000000004
# あれ?0.3じゃないの?

これは浮動小数点誤差という、コンピューターの仕組み上避けられない問題です。普通の計算なら気にならないかもしれませんが、お金の計算や精密な測定では大問題になります。

「レジで100円と200円の商品を買ったら、合計が300.0000001円になった」なんて、とても困りますよね。

そんな問題を解決してくれるのが、Pythonのdecimalモジュールです。これを使えば、小数の計算でも正確な結果が得られます。

この記事でわかること

  • 浮動小数点誤差がなぜ起こるのか
  • decimalモジュールの基本的な使い方
  • 実際にdecimalを使う方法
  • floatとdecimalの使い分け

こんな人におすすめ

  • お金の計算を扱うシステムを作る人
  • 精密な数値計算が必要な人
  • Pythonの数値処理をもっと理解したい人
  • 浮動小数点誤差に悩まされている人
スポンサーリンク

なぜ浮動小数点誤差が起こるの?

コンピューターの数値表現の仕組み

2進数での表現の限界: コンピューターは内部ですべてを2進数(0と1)で表現します。しかし、10進数の「0.1」を2進数で正確に表すことはできません。

身近な例で理解

  • 1/3を小数で表すと「0.33333…」と無限に続く
  • 同じように、0.1を2進数で表すと無限に続いてしまう
  • コンピューターは限られた桁数で近似するしかない

なぜ問題になるの?

# 一見正しそうに見える計算
price1 = 0.1  # 100円の1割引(10円)
price2 = 0.2  # 200円の1割引(20円)
total = price1 + price2  # 合計の割引額

print(f"割引合計: {total}円")
# 結果: 割引合計: 0.30000000000000004円

浮動小数点誤差が問題になる場面

金融・会計システム

  • 銀行の利息計算
  • 税額の計算
  • 請求書の金額計算
  • 家計簿アプリ

科学技術計算

  • 測定データの処理
  • 統計計算
  • 物理計算
  • 品質管理

ビジネスアプリケーション

  • ECサイトの価格計算
  • ポイント計算
  • 割引率の計算
  • 在庫管理

実際にどんな問題が起こるか

例1:消費税計算での問題

# 問題のあるコード
item_price = 100
tax_rate = 0.1  # 10%
tax = item_price * tax_rate
total = item_price + tax

print(f"税込み価格: {total}円")
# 結果: 税込み価格: 110.00000000000001円

例2:割引計算での問題

# 30%割引の計算
original_price = 1000
discount_rate = 0.3
discount_amount = original_price * discount_rate
final_price = original_price - discount_amount

print(f"割引後価格: {final_price}円")
# 結果: 割引後価格: 700.0000000000001円

これらの問題を解決するために、decimalモジュールが必要になります。

decimalモジュールとは?

decimalモジュールの特徴

正確な10進数計算

  • 10進数での正確な表現
  • 浮動小数点誤差なし
  • 人間が理解しやすい数値処理

カスタマイズ可能な精度

  • 小数点以下の桁数を指定可能
  • 丸め方法を選択可能
  • 計算精度を用途に応じて調整

豊富な数学操作

  • 基本的な四則演算
  • べき乗、平方根
  • 対数計算
  • 三角関数(一部)

decimalモジュールが解決する問題

問題解決の例

従来のfloat

print(0.1 + 0.2)
# 結果: 0.30000000000000004

decimalを使用

from decimal import Decimal

a = Decimal('0.1')
b = Decimal('0.2')
print(a + b)
# 結果: 0.3

いつdecimalを使うべき?

必ず使うべき場面

  • お金の計算
  • 税率の計算
  • 精密な測定値の処理
  • 会計・簿記システム

floatでも十分な場面

  • 科学計算(近似で十分)
  • グラフィック処理
  • 機械学習(一般的には)
  • ゲーム開発

判断の基準: 「1円でも誤差があったら困る」→ decimal 「多少の誤差は許容できる」→ float

次の章では、実際にdecimalモジュールの使い方を学んでいきましょう。

decimalモジュールの基本的な使い方

インポートと基本的な作成

モジュールのインポート

from decimal import Decimal

Decimalオブジェクトの作成

正しい方法(文字列を使用)

# 推奨: 文字列から作成
price = Decimal('10.50')
tax_rate = Decimal('0.1')
discount = Decimal('5.25')

print(price)  # 10.50
print(tax_rate)  # 0.1
print(discount)  # 5.25

間違った方法(floatを使用)

# 非推奨: floatから作成
price = Decimal(10.50)
print(price)  # 10.4999999999999982236431605997495353221893310546875
# → floatの誤差がそのまま持ち込まれる

なぜ文字列を使うの?

  • 文字列なら正確にその値を表現できる
  • floatを使うと、すでに誤差が含まれている
  • 「見たままの値」を確実に設定できる

基本的な四則演算

加算・減算・乗算・除算

from decimal import Decimal

# 商品価格の計算例
item1 = Decimal('100.50')  # 商品1
item2 = Decimal('200.25')  # 商品2
tax_rate = Decimal('0.1')  # 消費税10%

# 小計
subtotal = item1 + item2
print(f"小計: {subtotal}円")  # 小計: 300.75円

# 消費税
tax = subtotal * tax_rate
print(f"消費税: {tax}円")  # 消費税: 30.075円

# 合計
total = subtotal + tax
print(f"合計: {total}円")  # 合計: 330.825円

割引計算の例

# 20%割引の計算
original_price = Decimal('1000')
discount_rate = Decimal('0.2')

discount_amount = original_price * discount_rate
final_price = original_price - discount_amount

print(f"元の価格: {original_price}円")
print(f"割引額: {discount_amount}円")
print(f"最終価格: {final_price}円")

# 結果:
# 元の価格: 1000円
# 割引額: 200円
# 最終価格: 800円

比較演算

大小比較

price1 = Decimal('100.50')
price2 = Decimal('100.25')

if price1 > price2:
    print(f"{price1}円の方が高い")
else:
    print(f"{price2}円の方が高い")

# 結果: 100.50円の方が高い

等価比較

# 正確な比較ができる
a = Decimal('0.1') + Decimal('0.2')
b = Decimal('0.3')

print(a == b)  # True(正確に等しい)

# floatだと...
c = 0.1 + 0.2
d = 0.3
print(c == d)  # False(誤差のため等しくない)

文字列変換と表示

基本的な文字列変換

price = Decimal('123.456')

# そのまま表示
print(str(price))  # 123.456

# f-stringでの表示
print(f"価格: {price}円")  # 価格: 123.456円

フォーマットを指定した表示

price = Decimal('123.456')

# 小数点以下2桁で表示
print(f"価格: {price:.2f}円")  # 価格: 123.46円

# 小数点以下を0で埋める
print(f"価格: {price:08.2f}円")  # 価格: 00123.46円

型変換

他の型への変換

price = Decimal('123.45')

# intに変換(小数点以下切り捨て)
price_int = int(price)
print(price_int)  # 123

# floatに変換(精度が落ちる可能性あり)
price_float = float(price)
print(price_float)  # 123.45

注意点

  • intへの変換は小数点以下が切り捨てられる
  • floatへの変換は精度が落ちる可能性がある
  • 可能な限りDecimal型のまま計算を続ける

これで基本的な使い方はマスターできました。次は、より高度な丸め処理について学びましょう。

丸め処理と精度の詳細設定

基本的な丸め処理

quantize()メソッドの使用

説明:指定した精度で丸め処理を行う

基本的な使い方

from decimal import Decimal

price = Decimal('123.456')

# 小数点以下2桁に丸める
rounded_price = price.quantize(Decimal('0.01'))
print(rounded_price)  # 123.46

様々な精度での丸め

value = Decimal('123.456789')

# 整数に丸める
print(value.quantize(Decimal('1')))      # 123

# 小数点以下1桁
print(value.quantize(Decimal('0.1')))    # 123.5

# 小数点以下2桁
print(value.quantize(Decimal('0.01')))   # 123.46

# 小数点以下3桁
print(value.quantize(Decimal('0.001')))  # 123.457

丸め方法の指定

利用可能な丸めモード

from decimal import Decimal, ROUND_HALF_UP, ROUND_DOWN, ROUND_UP, ROUND_HALF_EVEN

value = Decimal('2.5')

# 四捨五入(一般的な丸め)
result1 = value.quantize(Decimal('1'), rounding=ROUND_HALF_UP)
print(f"四捨五入: {result1}")  # 3

# 切り捨て
result2 = value.quantize(Decimal('1'), rounding=ROUND_DOWN)
print(f"切り捨て: {result2}")  # 2

# 切り上げ
result3 = value.quantize(Decimal('1'), rounding=ROUND_UP)
print(f"切り上げ: {result3}")  # 3

# 銀行式丸め(偶数丸め)
result4 = value.quantize(Decimal('1'), rounding=ROUND_HALF_EVEN)
print(f"銀行式丸め: {result4}")  # 2

丸め方法の詳細説明

丸めモード説明例(2.5の場合)用途
ROUND_HALF_UP四捨五入3一般的な計算
ROUND_DOWN切り捨て2価格表示
ROUND_UP切り上げ3安全率を見込む計算
ROUND_HALF_EVEN銀行式丸め2金融計算(推奨)

実用的な丸め処理の例

消費税計算での丸め

from decimal import Decimal, ROUND_DOWN

# 商品価格
item_price = Decimal('198')
tax_rate = Decimal('0.10')  # 10%

# 消費税を計算(1円未満切り捨て)
tax = (item_price * tax_rate).quantize(Decimal('1'), rounding=ROUND_DOWN)
total = item_price + tax

print(f"商品価格: {item_price}円")
print(f"消費税: {tax}円")
print(f"税込み価格: {total}円")

# 結果:
# 商品価格: 198円
# 消費税: 19円  (19.8円を切り捨て)
# 税込み価格: 217円

ポイント還元率の計算

from decimal import Decimal, ROUND_HALF_UP

purchase_amount = Decimal('1234')
point_rate = Decimal('0.05')  # 5%還元

# ポイントを計算(小数点以下四捨五入)
points = (purchase_amount * point_rate).quantize(Decimal('1'), rounding=ROUND_HALF_UP)

print(f"購入金額: {purchase_amount}円")
print(f"還元ポイント: {points}ポイント")

# 結果:
# 購入金額: 1234円
# 還元ポイント: 62ポイント  (61.7ポイントを四捨五入)

全体的な精度設定

getcontext()による設定

説明:計算全体のデフォルト設定を変更

精度の設定

from decimal import getcontext

# 現在の設定を確認
print(getcontext())

# 精度を10桁に設定
getcontext().prec = 10

# 計算実行
result = Decimal('1') / Decimal('3')
print(result)  # 0.3333333333

丸め方法のデフォルト設定

from decimal import getcontext, ROUND_HALF_UP

# デフォルトの丸め方法を四捨五入に設定
getcontext().rounding = ROUND_HALF_UP

# この設定は以降のすべての計算に適用される
value = Decimal('2.5')
rounded = value.quantize(Decimal('1'))  # 丸め方法を指定しなくても四捨五入
print(rounded)  # 3

一時的な設定変更

localcontext()の使用

説明:一時的に設定を変更して、ブロック終了後に元に戻す

from decimal import Decimal, getcontext, localcontext, ROUND_DOWN

# 通常の設定
print("通常の精度:", getcontext().prec)

with localcontext() as ctx:
    # 一時的に精度を変更
    ctx.prec = 5
    ctx.rounding = ROUND_DOWN
    
    result = Decimal('1') / Decimal('3')
    print(f"一時的な設定での結果: {result}")  # 0.33333

# 元の設定に戻る
result2 = Decimal('1') / Decimal('3')
print(f"元の設定での結果: {result2}")

これで丸め処理の基本はマスターできました。次は、floatとの違いをより詳しく見ていきましょう。

floatとdecimalの詳細比較

精度と表現の違い

floatの限界

IEEE 754浮動小数点標準

  • 64ビットで数値を表現
  • 精度は約15〜17桁
  • 2進数での近似表現

decimalの利点

10進数での正確な表現

  • 任意の精度で計算可能
  • 10進数での厳密な表現
  • 人間の直感と一致する結果

実際の比較例

精度の比較

import math
from decimal import Decimal, getcontext

# floatの場合
float_pi = math.pi
print(f"float π: {float_pi}")
# float π: 3.141592653589793

# decimalの場合(精度を50桁に設定)
getcontext().prec = 50
decimal_pi = Decimal('3.1415926535897932384626433832795028841971693993751')
print(f"decimal π: {decimal_pi}")
# decimal π: 3.1415926535897932384626433832795028841971693993751

計算精度の違い

# float
a_float = 0.1
b_float = 0.2
c_float = 0.3
print(f"float: {a_float + b_float} == {c_float} は {a_float + b_float == c_float}")
# float: 0.30000000000000004 == 0.3 は False

# decimal
a_decimal = Decimal('0.1')
b_decimal = Decimal('0.2')
c_decimal = Decimal('0.3')
print(f"decimal: {a_decimal + b_decimal} == {c_decimal} は {a_decimal + b_decimal == c_decimal}")
# decimal: 0.3 == 0.3 は True

パフォーマンスの比較

速度の違い

測定コード

import time
from decimal import Decimal

# float での計算
start = time.time()
total_float = 0.0
for i in range(100000):
    total_float += 0.1
float_time = time.time() - start

# decimal での計算
start = time.time()
total_decimal = Decimal('0')
decimal_point_one = Decimal('0.1')
for i in range(100000):
    total_decimal += decimal_point_one
decimal_time = time.time() - start

print(f"float処理時間: {float_time:.4f}秒")
print(f"decimal処理時間: {decimal_time:.4f}秒")
print(f"decimal は float の約{decimal_time/float_time:.1f}倍の時間")

# 結果例:
# float処理時間: 0.0089秒
# decimal処理時間: 0.1234秒
# decimal は float の約13.9倍の時間

メモリ使用量の違い

import sys

float_value = 3.14159
decimal_value = Decimal('3.14159')

print(f"float のメモリ使用量: {sys.getsizeof(float_value)} bytes")
print(f"decimal のメモリ使用量: {sys.getsizeof(decimal_value)} bytes")

# 結果例:
# float のメモリ使用量: 24 bytes
# decimal のメモリ使用量: 104 bytes

用途別の使い分けガイドライン

floatを使うべき場面

科学計算

import math

# 物理計算の例
velocity = 30.0  # m/s
time = 2.5       # s
distance = velocity * time  # 近似で十分

# 統計計算の例
data = [1.2, 2.3, 3.4, 4.5, 5.6]
average = sum(data) / len(data)  # 近似で十分

グラフィック・ゲーム

# ゲームの座標計算
player_x = 100.5
player_y = 200.3
speed = 5.0

# 移動計算(完全な精度は不要)
new_x = player_x + speed
new_y = player_y + speed

機械学習

import numpy as np

# ニューラルネットワークの重み
weights = np.array([0.1, 0.2, 0.3, 0.4])  # 近似で十分
bias = 0.5

# 計算(完全な精度は不要)
output = np.dot(weights, [1, 1, 1, 1]) + bias

decimalを使うべき場面

金融計算

from decimal import Decimal

# 銀行の利息計算
principal = Decimal('1000000')  # 元金100万円
rate = Decimal('0.02')          # 年利2%
years = Decimal('5')            # 5年

# 複利計算(正確性が必要)
amount = principal * (Decimal('1') + rate) ** years
print(f"5年後の金額: {amount:.2f}円")

会計システム

# 請求書の計算
items = [
    {'name': '商品A', 'price': Decimal('1980'), 'qty': 3},
    {'name': '商品B', 'price': Decimal('2480'), 'qty': 2},
    {'name': '商品C', 'price': Decimal('980'), 'qty': 1},
]

total = Decimal('0')
for item in items:
    subtotal = item['price'] * item['qty']
    total += subtotal
    print(f"{item['name']}: {item['price']}円 × {item['qty']}個 = {subtotal}円")

tax = total * Decimal('0.1')  # 消費税
grand_total = total + tax

print(f"小計: {total}円")
print(f"消費税: {tax}円")
print(f"総計: {grand_total}円")

測定・計量システム

# 薬品の調合
ingredient_a = Decimal('15.25')  # g
ingredient_b = Decimal('8.75')   # g
ingredient_c = Decimal('2.50')   # g

total_weight = ingredient_a + ingredient_b + ingredient_c
print(f"総重量: {total_weight}g")  # 正確性が重要

変換時の注意点

安全な変換方法

文字列経由での変換

# float → decimal (推奨方法)
float_value = 123.45
decimal_value = Decimal(str(float_value))  # 文字列経由
print(decimal_value)  # 123.45

# 直接変換は避ける
decimal_bad = Decimal(float_value)  # 浮動小数点誤差が含まれる
print(decimal_bad)  # 123.45000000000000284...

型チェックと条件分岐

def safe_add(a, b):
    """安全な加算関数"""
    # 両方をDecimalに統一
    if not isinstance(a, Decimal):
        a = Decimal(str(a))
    if not isinstance(b, Decimal):
        b = Decimal(str(b))
    
    return a + b

# 使用例
result1 = safe_add(Decimal('100'), Decimal('200'))  # 300
result2 = safe_add(100, 200)                        # 300
result3 = safe_add(100.5, Decimal('200.25'))        # 300.75

このように、用途に応じて適切に使い分けることが重要です。

実際の活用例

ECサイトの価格計算システム

商品価格計算クラス

from decimal import Decimal, ROUND_HALF_UP

class PriceCalculator:
    """ECサイト用価格計算クラス"""
    
    def __init__(self, tax_rate='0.10'):
        self.tax_rate = Decimal(tax_rate)
    
    def calculate_subtotal(self, items):
        """小計を計算"""
        subtotal = Decimal('0')
        for item in items:
            price = Decimal(str(item['price']))
            quantity = Decimal(str(item['quantity']))
            subtotal += price * quantity
        return subtotal
    
    def calculate_tax(self, subtotal):
        """消費税を計算(1円未満四捨五入)"""
        tax = subtotal * self.tax_rate
        return tax.quantize(Decimal('1'), rounding=ROUND_HALF_UP)
    
    def apply_discount(self, amount, discount_rate):
        """割引を適用"""
        discount_rate = Decimal(str(discount_rate))
        discount = amount * discount_rate
        return amount - discount
    
    def calculate_total(self, items, discount_rate=0):
        """総額を計算"""
        subtotal = self.calculate_subtotal(items)
        
        # 割引適用
        if discount_rate > 0:
            subtotal = self.apply_discount(subtotal, discount_rate)
        
        # 消費税計算
        tax = self.calculate_tax(subtotal)
        total = subtotal + tax
        
        return {
            'subtotal': subtotal,
            'tax': tax,
            'total': total
        }

# 使用例
calculator = PriceCalculator()

# 購入商品
cart_items = [
    {'name': 'Tシャツ', 'price': 2980, 'quantity': 2},
    {'name': 'ジーンズ', 'price': 7980, 'quantity': 1},
    {'name': '靴下', 'price': 580, 'quantity': 3},
]

# 10%割引クーポン適用
result = calculator.calculate_total(cart_items, discount_rate=0.1)

print("=== 購入明細 ===")
for item in cart_items:
    item_total = Decimal(str(item['price'])) * Decimal(str(item['quantity']))
    print(f"{item['name']}: {item['price']}円 × {item['quantity']}個 = {item_total}円")

print(f"\n小計: {result['subtotal']}円")
print(f"消費税: {result['tax']}円")
print(f"総計: {result['total']}円")

給与計算システム

給与計算の実装例

from decimal import Decimal, ROUND_DOWN

class SalaryCalculator:
    """給与計算クラス"""
    
    def __init__(self):
        # 税率・保険料率(2024年基準)
        self.income_tax_rate = Decimal('0.05')      # 所得税(簡素化)
        self.resident_tax_rate = Decimal('0.10')    # 住民税(簡素化)
        self.health_insurance_rate = Decimal('0.05') # 健康保険
        self.pension_rate = Decimal('0.0915')        # 厚生年金
        self.employment_insurance_rate = Decimal('0.003') # 雇用保険
    
    def calculate_social_insurance(self, base_salary):
        """社会保険料を計算"""
        health_insurance = (base_salary * self.health_insurance_rate).quantize(
            Decimal('1'), rounding=ROUND_DOWN)
        pension = (base_salary * self.pension_rate).quantize(
            Decimal('1'), rounding=ROUND_DOWN)
        employment_insurance = (base_salary * self.employment_insurance_rate).quantize(
            Decimal('1'), rounding=ROUND_DOWN)
        
        return {
            'health_insurance': health_insurance,
            'pension': pension,
            'employment_insurance': employment_insurance,
            'total': health_insurance + pension + employment_insurance
        }
    
    def calculate_tax(self, taxable_income):
        """税金を計算"""
        income_tax = (taxable_income * self.income_tax_rate).quantize(
            Decimal('1'), rounding=ROUND_DOWN)
        resident_tax = (taxable_income * self.resident_tax_rate).quantize(
            Decimal('1'), rounding=ROUND_DOWN)
        
        return {
            'income_tax': income_tax,
            'resident_tax': resident_tax,
            'total': income_tax + resident_tax
        }
    
    def calculate_net_salary(self, base_salary):
        """手取り給与を計算"""
        base_salary = Decimal(str(base_salary))
        
        # 社会保険料計算
        social_insurance = self.calculate_social_insurance(base_salary)
        
        # 課税所得(基本給 - 社会保険料)
        taxable_income = base_salary - social_insurance['total']
        
        # 税金計算
        tax = self.calculate_tax(taxable_income)
        
        # 手取り額
        net_salary = base_salary - social_insurance['total'] - tax['total']
        
        return {
            'base_salary': base_salary,
            'social_insurance': social_insurance,
            'tax': tax,
            'net_salary': net_salary
        }

# 使用例
calculator = SalaryCalculator()
monthly_salary = 300000  # 月給30万円

result = calculator.calculate_net_salary(monthly_salary)

print("=== 給与明細 ===")
print(f"基本給: {result['base_salary']:,}円")
print("\n【控除項目】")
print(f"健康保険料: {result['social_insurance']['health_insurance']:,}円")
print(f"厚生年金保険料: {result['social_insurance']['pension']:,}円")
print(f"雇用保険料: {result['social_insurance']['employment_insurance']:,}円")
print(f"所得税: {result['tax']['income_tax']:,}円")
print(f"住民税: {result['tax']['resident_tax']:,}円")
print(f"\n控除合計: {result['social_insurance']['total'] + result['tax']['total']:,}円")
print(f"手取り額: {result['net_salary']:,}円")

投資収益計算システム

複利計算の実装

from decimal import Decimal, getcontext

class InvestmentCalculator:
    """投資計算クラス"""
    
    def __init__(self, precision=10):
        # 計算精度を設定
        getcontext().prec = precision
    
    def compound_interest(self, principal, annual_rate, years, compound_frequency=1):
        """複利計算
        
        Args:
            principal: 元本
            annual_rate: 年利率(小数)
            years: 投資期間(年)
            compound_frequency: 年間複利回数(1=年1回、12=月1回)
        """
        principal = Decimal(str(principal))
        annual_rate = Decimal(str(annual_rate))
        years = Decimal(str(years))
        compound_frequency = Decimal(str(compound_frequency))
        
        # 複利計算式: A = P(1 + r/n)^(nt)
        rate_per_period = annual_rate / compound_frequency
        total_periods = compound_frequency * years
        
        # (1 + r/n)^(nt) を計算
        growth_factor = (Decimal('1') + rate_per_period) ** total_periods
        final_amount = principal * growth_factor
        
        return {
            'principal': principal,
            'final_amount': final_amount,
            'total_return': final_amount - principal,
            'return_rate': ((final_amount / principal) - Decimal('1')) * Decimal('100')
        }
    
    def monthly_investment(self, monthly_amount, annual_rate, years):
        """積立投資計算"""
        monthly_amount = Decimal(str(monthly_amount))
        monthly_rate = Decimal(str(annual_rate)) / Decimal('12')
        total_months = Decimal(str(years)) * Decimal('12')
        
        # 等比級数の和の公式を使用
        if monthly_rate == 0:
            final_amount = monthly_amount * total_months
        else:
            # FV = PMT × (((1 + r)^n - 1) / r)
            growth_factor = (Decimal('1') + monthly_rate) ** total_months
            final_amount = monthly_amount * ((growth_factor - Decimal('1')) / monthly_rate)
        
        total_invested = monthly_amount * total_months
        
        return {
            'monthly_amount': monthly_amount,
            'total_invested': total_invested,
            'final_amount': final_amount,
            'total_return': final_amount - total_invested,
            'years': years
        }

# 使用例
calc = InvestmentCalculator()

# 1. 一括投資の例
lump_sum = calc.compound_interest(
    principal=1000000,    # 100万円
    annual_rate=0.05,     # 年利5%
    years=10,             # 10年間
    compound_frequency=12 # 月複利
)

print("=== 一括投資シミュレーション ===")
print(f"元本: {lump_sum['principal']:,}円")
print(f"最終金額: {lump_sum['final_amount']:.0f}円")
print(f"利益: {lump_sum['total_return']:.0f}円")
print(f"収益率: {lump_sum['return_rate']:.2f}%")

# 2. 積立投資の例
monthly_invest = calc.monthly_investment(
    monthly_amount=50000,  # 月5万円
    annual_rate=0.05,      # 年利5%
    years=20               # 20年間
)

print("\n=== 積立投資シミュレーション ===")
print(f"月額投資: {monthly_invest['monthly_amount']:,}円")
print(f"投資期間: {monthly_invest['years']}年")
print(f"投資総額: {monthly_invest['total_invested']:,}円")
print(f"最終金額: {monthly_invest['final_amount']:.0f}円")
print(f"利益: {monthly_invest['total_return']:.0f}円")

在庫管理システム

在庫コスト計算

from decimal import Decimal, ROUND_HALF_UP
from datetime import datetime, timedelta

class InventoryManager:
    """在庫管理クラス"""
    
    def __init__(self):
        self.inventory = {}
    
    def add_stock(self, product_id, quantity, unit_cost, date=None):
        """在庫追加(移動平均法)"""
        if date is None:
            date = datetime.now()
        
        quantity = Decimal(str(quantity))
        unit_cost = Decimal(str(unit_cost))
        
        if product_id not in self.inventory:
            self.inventory[product_id] = {
                'quantity': Decimal('0'),
                'total_cost': Decimal('0'),
                'average_cost': Decimal('0'),
                'transactions': []
            }
        
        current = self.inventory[product_id]
        
        # 新しい在庫の追加
        new_total_quantity = current['quantity'] + quantity
        new_total_cost = current['total_cost'] + (quantity * unit_cost)
        
        # 移動平均単価の計算
        if new_total_quantity > 0:
            new_average_cost = new_total_cost / new_total_quantity
        else:
            new_average_cost = Decimal('0')
        
        # 在庫情報更新
        current['quantity'] = new_total_quantity
        current['total_cost'] = new_total_cost
        current['average_cost'] = new_average_cost.quantize(
            Decimal('0.01'), rounding=ROUND_HALF_UP)
        
        # 取引履歴追加
        current['transactions'].append({
            'date': date,
            'type': 'IN',
            'quantity': quantity,
            'unit_cost': unit_cost,
            'balance': new_total_quantity
        })
        
        return current['average_cost']
    
    def remove_stock(self, product_id, quantity, date=None):
        """在庫出庫"""
        if date is None:
            date = datetime.now()
        
        quantity = Decimal(str(quantity))
        
        if product_id not in self.inventory:
            raise ValueError(f"商品 {product_id} が見つかりません")
        
        current = self.inventory[product_id]
        
        if current['quantity'] < quantity:
            raise ValueError(f"在庫不足: 現在庫{current['quantity']}, 出庫要求{quantity}")
        
        # 出庫コスト(移動平均単価 × 出庫数量)
        outbound_cost = current['average_cost'] * quantity
        
        # 在庫更新
        current['quantity'] -= quantity
        current['total_cost'] -= outbound_cost
        
        # 取引履歴追加
        current['transactions'].append({
            'date': date,
            'type': 'OUT',
            'quantity': quantity,
            'unit_cost': current['average_cost'],
            'balance': current['quantity']
        })
        
        return outbound_cost
    
    def get_inventory_value(self, product_id=None):
        """在庫評価額を取得"""
        if product_id:
            if product_id in self.inventory:
                item = self.inventory[product_id]
                return item['quantity'] * item['average_cost']
            return Decimal('0')
        else:
            total_value = Decimal('0')
            for item in self.inventory.values():
                total_value += item['quantity'] * item['average_cost']
            return total_value

# 使用例
inventory = InventoryManager()

# 商品A の在庫取引
print("=== 商品A 在庫管理 ===")

# 入庫1: 100個 @ 1,000円
avg_cost1 = inventory.add_stock('A001', 100, 1000)
print(f"入庫1: 100個 @ 1,000円 → 平均単価: {avg_cost1}円")

# 入庫2: 200個 @ 1,200円
avg_cost2 = inventory.add_stock('A001', 200, 1200)
print(f"入庫2: 200個 @ 1,200円 → 平均単価: {avg_cost2}円")

# 出庫: 150個
outbound_cost = inventory.remove_stock('A001', 150)
print(f"出庫: 150個 → 出庫コスト: {outbound_cost}円")

# 現在の在庫状況
current_inventory = inventory.inventory['A001']
inventory_value = inventory.get_inventory_value('A001')

print(f"\n現在庫: {current_inventory['quantity']}個")
print(f"平均単価: {current_inventory['average_cost']}円")
print(f"在庫評価額: {inventory_value}円")

よくある間違いとベストプラクティス

よくある間違い

間違い1:floatからの直接変換

悪い例

# ❌ 浮動小数点誤差が持ち込まれる
price = 19.99
decimal_price = Decimal(price)
print(decimal_price)
# 19.9899999999999984456877655247808434069156646728515625

正しい例

# ✅ 文字列経由で正確に変換
price = 19.99
decimal_price = Decimal(str(price))
print(decimal_price)
# 19.99

間違い2:丸め処理を忘れる

悪い例

# ❌ 丸め処理なしだと予期しない精度
price = Decimal('100')
tax_rate = Decimal('0.08')
tax = price * tax_rate / Decimal('3')  # 3分割
print(tax)  # 2.666666666666666666666666667

正しい例

# ✅ 適切な丸め処理
price = Decimal('100')
tax_rate = Decimal('0.08')
tax = price * tax_rate / Decimal('3')
tax_rounded = tax.quantize(Decimal('0.01'), rounding=ROUND_HALF_UP)
print(tax_rounded)  # 2.67

間違い3:型の混在

悪い例

# ❌ DecimalとFloatの混在
decimal_price = Decimal('100.50')
float_tax = 0.1
total = decimal_price + float_tax  # 型が混在
print(type(total))  # <class 'decimal.Decimal'> だが精度が落ちる

正しい例

# ✅ 型を統一
decimal_price = Decimal('100.50')
decimal_tax = Decimal('0.1')
total = decimal_price + decimal_tax
print(total)  # 100.6

ベストプラクティス

1. 一貫した型の使用

from decimal import Decimal

class MoneyCalculator:
    """金額計算用クラス"""
    
    @staticmethod
    def to_decimal(value):
        """安全にDecimalに変換"""
        if isinstance(value, Decimal):
            return value
        return Decimal(str(value))
    
    def add(self, a, b):
        """安全な加算"""
        return self.to_decimal(a) + self.to_decimal(b)
    
    def multiply(self, a, b):
        """安全な乗算"""
        return self.to_decimal(a) * self.to_decimal(b)

# 使用例
calc = MoneyCalculator()
result = calc.add(100.5, Decimal('200.25'))  # 型を気にしなくて良い

2. 設定の管理

from decimal import getcontext, localcontext, ROUND_HALF_UP
from contextlib import contextmanager

@contextmanager
def money_context():
    """金額計算用のコンテキスト"""
    with localcontext() as ctx:
        ctx.prec = 28  # 高精度
        ctx.rounding = ROUND_HALF_UP  # 四捨五入
        yield ctx

# 使用例
with money_context():
    result = Decimal('100') / Decimal('3')
    rounded = result.quantize(Decimal('0.01'))
    print(rounded)  # 33.33

3. エラーハンドリング

from decimal import Decimal, InvalidOperation

def safe_decimal_calculation(value1, value2, operation):
    """安全なDecimal計算"""
    try:
        d1 = Decimal(str(value1))
        d2 = Decimal(str(value2))
        
        if operation == 'add':
            return d1 + d2
        elif operation == 'subtract':
            return d1 - d2
        elif operation == 'multiply':
            return d1 * d2
        elif operation == 'divide':
            if d2 == 0:
                raise ValueError("0で除算はできません")
            return d1 / d2
        else:
            raise ValueError(f"未対応の操作: {operation}")
    
    except InvalidOperation as e:
        raise ValueError(f"無効な数値: {e}")
    except Exception as e:
        raise ValueError(f"計算エラー: {e}")

# 使用例
try:
    result = safe_decimal_calculation("100.5", "0.1", "multiply")
    print(result)  # 10.05
except ValueError as e:
    print(f"エラー: {e}")

4. 設定のカプセル化

class PrecisionCalculator:
    """精度管理付き計算クラス"""
    
    def __init__(self, precision=10, rounding=ROUND_HALF_UP):
        self.precision = precision
        self.rounding = rounding
    
    def calculate(self, func, *args, **kwargs):
        """指定された精度で計算を実行"""
        with localcontext() as ctx:
            ctx.prec = self.precision
            ctx.rounding = self.rounding
            return func(*args, **kwargs)
    
    def format_currency(self, amount, currency_symbol='¥'):
        """通貨形式でフォーマット"""
        formatted = f"{currency_symbol}{amount:,.2f}"
        return formatted

# 使用例
calc = PrecisionCalculator(precision=20)

def complex_calculation():
    return Decimal('1') / Decimal('3') * Decimal('9999.99')

result = calc.calculate(complex_calculation)
formatted = calc.format_currency(result)
print(formatted)  # ¥3,333.33

まとめ:decimalモジュールで正確な計算を

重要なポイントをおさらい

decimalモジュールの基本

  • 10進数での正確な計算:浮動小数点誤差を完全に回避
  • 文字列初期化Decimal('0.1')で正確な値を設定
  • カスタマイズ可能:精度と丸め方法を用途に応じて調整

実装のベストプラクティス

  • 型の統一:計算全体でDecimal型を維持
  • エラーハンドリング:適切な例外処理
  • 設定管理:contextを使った一時的な設定変更

コメント

タイトルとURLをコピーしました