0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ブラウザだけでCSVの重複候補を探す:TypeScriptで日本語名寄せの簡易デモを作る

0
Last updated at Posted at 2026-07-20

Excelの「重複の削除」は、セルの内容が完全に同じ行を整理するには便利です。しかし、次のような表記揺れは別の値として残ります。

齋藤 太郎              / 斎藤太郎
神宮前1丁目2番3号      / 神宮前1-2-3
090-1234-5678          / 09012345678

そこで、CSVをサーバーへ送らず、ブラウザ内で正規化・比較・採点する小さなデモをReactとTypeScriptで作りました。本記事では実装の中心部分と、実運用へ進む前に必要な安全策をまとめます。

処理の流れ

処理は次の4段階です。

  1. <input type="file"> でCSVを選ぶ
  2. File.text() でブラウザ内へ読み込む
  3. 氏名・住所・電話番号・メールを比較用の表記へ正規化する
  4. 複数項目の一致を採点し、しきい値以上の組だけ候補として表示する

デモでは計算量と誤操作の影響を抑えるため、先頭200行だけを対象にしています。選択したCSVの内容をAPIへ送る処理は実装していません。

列名の違いを吸収する

名簿ごとに列名が異なるため、用途別に別名を用意しました。

const aliases = {
  id: ["id", "顧客id", "顧客番号", "会員番号", "管理番号"],
  name: ["氏名", "名前", "顧客名", "会員名", "担当者名"],
  address: ["住所", "所在地", "送付先", "居住地"],
  phone: ["電話番号", "電話", "tel", "携帯番号", "携帯"],
  email: ["メール", "メールアドレス", "email", "e-mail"],
};

type Row = Record<string, string>;

function findColumn(row: Row, kind: keyof typeof aliases) {
  const wanted = aliases[kind];
  return Object.keys(row).find((key) =>
    wanted.includes(key.trim().toLowerCase())
  );
}

この方式は単純ですが、列名が想定外なら一致しません。製品化するなら、候補列を提示し、利用者がマッピングを確認できる画面が必要です。

日本語の表記を比較用に揃える

まず、Unicode正規化、空白・句読点の除去、小文字化を共通処理にします。

function common(value: string) {
  return value
    .normalize("NFKC")
    .toLowerCase()
    .replace(/[\s ・,,.。]/g, "");
}

function normalizeName(value: string) {
  return common(value)
    .replace(/[齋齊斉]/g, "")
    .replace(/髙/g, "")
    .replace(/﨑/g, "")
    .replace(/(株式会社|有限会社|合同会社|\(\)|㈱)/g, "");
}

function digits(value: string) {
  return value.normalize("NFKC").replace(/\D/g, "");
}

住所は「丁目・番・号」をハイフンへ寄せます。

function normalizeAddress(value: string) {
  return common(value)
    .replace(/[一壱]丁目/g, "1-")
    .replace(/[二弐]丁目/g, "2-")
    .replace(/[三参]丁目/g, "3-")
    .replace(/四丁目/g, "4-")
    .replace(/五丁目/g, "5-")
    .replace(/([0-9]+)番地?([0-9]+)号/g, "$1-$2")
    .replace(/([0-9]+)番/g, "$1-")
    .replace(/([0-9]+)号/g, "$1")
    .replace(/-+/g, "-")
    .replace(/-$/, "");
}

これは価値検証用の簡易ルールです。漢数字全般、都道府県の省略、建物名、部屋番号、旧字体の網羅には対応していません。正規化結果をそのまま原本へ書き戻さないことも重要です。

1項目だけで決めず、根拠を残す

同姓同名を誤って統合しないよう、氏名だけの一致では候補にしません。複数項目を加点し、一致した根拠も保存します。

type Match = {
  left: Row;
  right: Row;
  score: number;
  reasons: string[];
};

function analyze(left: Row, right: Row): Match | null {
  let score = 0;
  const reasons: string[] = [];

  if (normalizeName(left.氏名) === normalizeName(right.氏名)) {
    score += 28;
    reasons.push("氏名");
  }
  if (normalizeAddress(left.住所) === normalizeAddress(right.住所)) {
    score += 27;
    reasons.push("住所");
  }
  if (digits(left.電話番号) === digits(right.電話番号)) {
    score += 27;
    reasons.push("電話");
  }
  if (common(left.メール) === common(right.メール)) {
    score += 30;
    reasons.push("メール");
  }

  if (score < 50) return null;
  return { left, right, score: Math.min(99, score), reasons };
}

実際のコードでは空文字同士を一致扱いしないよう、正規化後の値が存在することも確認しています。点数と50点というしきい値に統計的な根拠はなく、あくまでUIと需要を検証するための仮置きです。

全組み合わせ比較の限界

デモでは実装を単純にするため、全行の組み合わせを比較しています。

const matches: Match[] = [];

for (let left = 0; left < rows.length; left += 1) {
  for (let right = left + 1; right < rows.length; right += 1) {
    const match = analyze(rows[left], rows[right]);
    if (match) matches.push(match);
  }
}

計算量は O(n²) です。200行なら比較回数は19,900回ですが、1万行では約5,000万回になるため、そのままでは使えません。

件数を増やす場合は、電話番号の下4桁、メールドメイン、郵便番号、氏名の読みなどで比較対象を先に分割する「ブロッキング」が必要です。その後、ブロック内だけを類似度計算すれば候補数を減らせます。

実運用で必要な安全策

名寄せでは、重複の見逃しより「別人を同じ人として統合する誤り」のほうが復旧しにくい場合があります。最低限、次を守る設計にします。

  • 元のCSVを上書きしない
  • 候補、スコア、一致した項目を別ファイルへ出力する
  • 自動統合と要確認のしきい値を分ける
  • 空欄同士を一致扱いしない
  • 正規化前後の値と処理日時を監査用に残す
  • 実データで適合率・再現率を測るまでAIやルールに最終決定させない

動作デモ

実装した簡易デモは以下で試せます。

現在は開発前の需要検証で、ダウンロードできる製品は存在しません。デモは製品版AIではなく、TypeScriptで書いた簡易ルール判定です。CSV解析はブラウザ内で行い、通知登録を送信した場合だけ回答内容が需要集計用データベースへ保存されます。

この記事は、完全自動で需要を検証する実験の一環として、AIによる調査・文章作成支援を利用して作成しました。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?