1.5kg と 1500g と 1,500。同じものを指しているかもしれませんが、そのまま集計すると桁違いの誤りになります。
は地味な工程ですが、事故が起きたときの影響が大きい領域です。この回は、通貨・数量・日時について整理します。
1. 混在に気づく
まず、混ざっていることに気づく必要があります。値を眺めても分かりません。分布を見ます。
import numpy as npimport pandas as pd
def suspect_unit_mix(series: pd.Series, name: str = "") -> None: """値の桁の分布を見て、単位の混在を疑う。""" s = pd.to_numeric(series, errors="coerce").dropna() s = s[s > 0] if s.empty: print(f"{name}: 数値がありません") return
# 桁数ごとの件数を数える digits = np.floor(np.log10(s)).astype(int) counts = digits.value_counts().sort_index()
print(f"=== {name} ===") print(f" 最小 {s.min():,.2f} / 最大 {s.max():,.2f} " f"(比 {s.max() / s.min():,.0f} 倍)") print(" 桁ごとの件数:") for d, n in counts.items(): print(f" 10^{d:<3d} {n:6d} 件")
# 桁が離れた集団が2つ以上あれば疑わしい gaps = np.diff(counts.index.values) if (gaps >= 2).any(): print(" → 桁の離れた集団があります。単位の混在を疑ってください")
# 例: kg と g が混ざっているデータmixed = pd.Series([1.2, 1.5, 0.8, 1200, 1500, 800, 1.1, 950])suspect_unit_mix(mixed, "weight")桁の離れた集団が2つあるなら、単位の混在を強く疑います。上の例では 1前後 と 1000前後 の2つの山ができ、kg と g が混ざっていることが見えます。これは 統計入門 EP.04 で扱った「二峰性」と同じ形で、分布の形を見れば気づける種類の異常です。
簡便な方法として、最大値と最小値の比を見るのも有効です。同じ性質の値のはずなのに 1000倍以上の開きがあれば、単位の混在か、外れ値か、どちらかです。どちらにせよ調査対象になります。
2. 通貨と金額
金額で最も多い事故は、小数で持つことによる誤差です。計算を重ねるとずれます。
from decimal import Decimal
# 浮動小数点で 0.1 を10回足すtotal_float = 0.0for _ in range(10): total_float += 0.1print(f"浮動小数点: {total_float!r}")print(f" 1.0 と等しいか: {total_float == 1.0}")
# 整数(最小単位)で持つtotal_int = sum(1 for _ in range(10)) # 0.1円を1として扱うprint(f"整数(0.1円単位): {total_int} → {total_int / 10} 円")
# Decimal を使うtotal_dec = sum(Decimal("0.1") for _ in range(10))print(f"Decimal: {total_dec} / 1と等しいか: {total_dec == Decimal('1.0')}")浮動小数点では 1.0 になりません。1回なら誤差は無視できますが、何万件も集計すると顕在化します。「合計が1円合わない」という問い合わせの原因になります。
| 持ち方 | 誤差 | 扱いやすさ | 推奨 |
|---|---|---|---|
| 浮動小数点 | 出る | 簡単 | 使わない |
| 最小単位の整数 | 出ない | 変換が要る | 推奨 |
| 十進の型(Decimal) | 出ない | やや遅い | 推奨 |
最小単位の整数で持つのが最も確実です。円なら円単位の整数。それより細かい単位が必要なら(税計算など)、何倍で持つかを明示的に決めておきます。列名に単位を入れる(`amount_jpy`)と、取り違えが減ります。
通貨が複数ある場合は、さらに注意が要ります。通貨の種類を保持せずに金額だけを持つと、後から取り返しがつきません。そして換算した値だけを残すのも危険です。レートが変われば結果が変わるため、元の通貨と金額、換算に使ったレートと時点を揃えて持ってください。
3. 日時とタイムゾーン
日時は、タイムゾーンの情報を持たない値が最も危険です。後から「これは何時のことか」が確定できません。
| 持ち方 | 問題 |
|---|---|
| タイムゾーンなしの日時 | 何時のことか確定できない |
| 現地時間で保存 | 夏時間や地域をまたぐと破綻 |
| 文字列で保存 | 形式が揺れる。比較できない |
| 世界標準時で保存し、表示時に変換 | 推奨 |
from datetime import datetime, timezone, timedeltaimport re
JST = timezone(timedelta(hours=9))
# 実データによくある形式PATTERNS = [ ("%Y-%m-%d %H:%M:%S", r"^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$"), ("%Y/%m/%d %H:%M", r"^\d{4}/\d{1,2}/\d{1,2} \d{1,2}:\d{2}$"), ("%Y%m%d%H%M%S", r"^\d{14}$"), ("%Y-%m-%d", r"^\d{4}-\d{2}-\d{2}$"),]
def normalize_datetime(value: str, assume_tz=JST) -> datetime | None: """様々な形式を、世界標準時の日時に揃える。
タイムゾーンの情報がない値は assume_tz とみなす。 この「みなし」は必ず記録に残すこと。 """ v = str(value).strip() for fmt, pattern in PATTERNS: if re.match(pattern, v): dt = datetime.strptime(v, fmt) return dt.replace(tzinfo=assume_tz).astimezone(timezone.utc) return None
for v in ["2026-09-01 10:30:00", "2026/9/1 10:30", "20260901103000", "2026-09-01", "令和8年9月1日"]: r = normalize_datetime(v) print(f"{v:24s} → {r.isoformat() if r else '変換できません'}")変換できないものを `None` で返しているのが要点です。EP.17・EP.20 と同じで、黙って捨てず、変換できなかったことを残します。上の例では和暦が変換できないので、その形式への対応が要ると分かります。
タイムゾーンのない値を「日本時間だろう」とみなす処理は、実務では避けられません。ただしみなしたという事実を記録に残してください。後から「この時刻は本当に日本時間か」と問われたときに答えられます。時刻の取り違えは データ基盤トラブル事件簿 EP.10 のような形で顕在化します。
4. 数量と単位
重量・長さ・容量などは、単位付きの文字列で入ってくることがあります。数値と単位を分離してから揃えます。
import re
# 基準単位に変換するための倍率UNIT_FACTORS = { "mg": 0.001, "g": 1, "kg": 1000, "t": 1_000_000, # 基準: グラム "グラム": 1, "キロ": 1000, "キログラム": 1000,}
PATTERN = re.compile(r"^\s*([\d,]+(?:\.\d+)?)\s*([A-Za-zぁ-んァ-ヶ]+)?\s*$")
def to_grams(value: str, default_unit: str | None = None): """(グラム数, 使った単位, エラー) を返す。""" m = PATTERN.match(str(value)) if not m: return None, None, f"形式が読めない: {value!r}"
num = float(m.group(1).replace(",", "")) unit = m.group(2) or default_unit
if unit is None: return None, None, f"単位が不明: {value!r}" if unit not in UNIT_FACTORS: return None, unit, f"未知の単位: {unit!r}"
return num * UNIT_FACTORS[unit], unit, None
for v in ["1.5kg", "1500g", "1,500", "2t", "500ミリ", "3キロ"]: grams, unit, err = to_grams(v, default_unit="g") if err: print(f"{v:10s} → {err}") else: print(f"{v:10s} → {grams:>12,.1f} g (単位: {unit})")単位が書かれていない値の扱いが判断のいる箇所です。`default_unit` として何を仮定するかは、データの出どころによって決めるしかありません。そして、仮定したことを記録に残します。
「500ミリ」のように未知の単位が出てきたら、エラーとして残します。黙って無視すると、その行だけ値が消えます。しかも件数は変わらないので、合計だけが小さくなるという形で現れます。これは EP.22 で扱う「静かに壊れる」の典型例です。
5. 揃えたことを記録する
EP.17 で扱った「推定値であることを保つ」と同じで、変換したことも記録に残します。
| 残す情報 | 後から役立つ場面 |
|---|---|
| 元の値 | 変換が誤っていたとき、やり直せる |
| 使った単位 | 仮定が正しかったかを検証できる |
| 仮定したかどうか | 単位が書かれていなかった行を特定できる |
| 変換できなかった件数 | 取りこぼしに気づける |
元の値を残すのが最も重要です。変換規則が誤っていたと後から分かったとき、元があればやり直せます。上書きしてしまうと、取り返しがつきません。容量を惜しんで元の値を捨てると、後で数倍の工数を払うことになります。
6. 検査を工程に入れる
最後に、混在が再発しないための検査です。上流のデータは変わります。
- 桁の分布を定期的に見る — 新しい単位が混ざり始めたら気づける
- 変換できなかった件数を監視する — 増えたら形式が変わった
- 値の範囲を検査する — 想定外の桁を弾く
- 上流と取り決めを作る — として明示
4番目が根本的な対処です。上流に「この列は g 単位の整数」と明示してもらう。取り決めがあれば、破られたときに検知できます。取り決めがないと、変わったことすら気づけません。次回(EP.22)で扱いますが、この取り決めは検査の仕様としてコードに書けるので、文書として持つより確実です。
混在は桁の分布(離れた集団が2つ)で気づける。金額は最小単位の整数で持つ(浮動小数点は使わない)。日時は世界標準時で保存し、表示時に変換。タイムゾーンの「みなし」は必ず記録に残す。単位付きの値は変換できなかったものを残す。そして元の値を保持しておけば、規則が誤っていてもやり直せます。
この記事の感想を教えてください
あなたの 1 クリックで、本当にこの記事は更新されます。「もっと詳しく」「続編希望」が一定数集まった記事は、 ふくふくが 実際に内容を拡充したり続編記事を公開 します。 送信したリアクションはお使いのブラウザに記録され、再カウントされません。