ふくふくHukuhuku Inc.
EP.21Prep 13分公開: 2026-09-01

単位と桁を揃える ── 地味だが事故の多い領域

1.5kg と 1500g と 1,500。同じものを指しているのに、揃えないまま集計すると桁違いの誤りになります。通貨・重量・日時の正規化を扱います。

#前処理#正規化#データ品質#Python
執筆 / 監修
松尾 亮合同会社ふくふく 代表社員

データ基盤・データパイプライン構築 / BI / 生成 AI 活用支援を専門とするエンジニア (28 年)。 本記事は AI 利用ポリシーに基づき、生成 AI の補助で執筆 → 人間が監修・編集して公開しています。

プロフィール詳細
シェア

1.5kg1500g1,500。同じものを指しているかもしれませんが、そのまま集計すると桁違いの誤りになります。

は地味な工程ですが、事故が起きたときの影響が大きい領域です。この回は、通貨・数量・日時について整理します。

1. 混在に気づく

まず、混ざっていることに気づく必要があります。値を眺めても分かりません。分布を見ます

単位の混在を、桁の分布から疑う
Python
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. 通貨と金額

金額で最も多い事故は、小数で持つことによる誤差です。計算を重ねるとずれます。

小数で金額を扱うとずれる
Python
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. 日時とタイムゾーン

日時は、タイムゾーンの情報を持たない値が最も危険です。後から「これは何時のことか」が確定できません。

日時の持ち方
持ち方問題
タイムゾーンなしの日時何時のことか確定できない
現地時間で保存夏時間や地域をまたぐと破綻
文字列で保存形式が揺れる。比較できない
世界標準時で保存し、表示時に変換推奨
日時の正規化
Python
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. 数量と単位

重量・長さ・容量などは、単位付きの文字列で入ってくることがあります。数値と単位を分離してから揃えます。

単位付きの値を、基準単位に揃える
Python
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 クリックで、本当にこの記事は更新されます。「もっと詳しく」「続編希望」が一定数集まった記事は、 ふくふくが 実際に内容を拡充したり続編記事を公開 します。 送信したリアクションはお使いのブラウザに記録され、再カウントされません。

シリーズの外も探す:

まずは、現状を聞かせてください。

要件が固まっていなくて大丈夫です。現状診断と方針提案までを無料でお手伝いします。

無料相談フォームへ hello [at] hukuhuku [dot] co [dot] jp