はじめに
仕事でDatabricks を使ったデータ基盤の支援で気づいたことが「Databricks のデータの品質チェックが、日本のデータに合っていない」ということです。
たとえば法人番号。「13桁の数字ならOK」というチェックはよく見かけますが、実は法人番号には打ち間違いを見抜くためのチェックディジットが入っていて、13桁あっても間違っている番号は普通にあり得ます。和暦も同じで、「令和元年4月30日」のように、形は正しいのに実際には存在しない日付があります。社内データをAIに読ませる流れが広がる中で、こういう「それっぽいけど間違っている値」をどう拾うかは、意外と大事なテーマだと思っています。
※こちらは非公式機能でありDatabricks公式機能ではありません。
そこでこの記事では、Databricks でこうしたチェックをするときにハマりがちなポイントを、国税庁や国立天文台などの公式資料を確認しながら7つにまとめました。あわせて、これらをまとめて解決する OSS「jpdq-oga」を作って公開したので、使い方を紹介します。Databricks でデータ基盤を作っている方や、DQX を使っている方の参考になればうれしいです。
検証環境について: 記事中の実行結果は、ローカルの Apache Spark 3.5.3(ANSI モードは無効・有効の両方)、pandas 3.0.6、databricks-labs-dqx 0.16.0 で、jpdq-oga を実際に実行して得たものです。エラーメッセージの文言は Spark のバージョンで変わります。あわせて、Databricks Free Edition(サーバーレス、Spark Connect)の実機でも、同じコードを実行して確認しました。 リポジトリのノートブックの 4 行のサンプルで、DQX のルールが期待どおりに動くこと(正常 2 行、違反メッセージ 8 件)と、「使い方3」の基準日による判定の入れ替わりを確認しています(この記事の 5 件の例そのものを実機で流したわけではありません)。
法人番号の落とし穴
落とし穴1:「13桁の数字」の正規表現は打ち間違いを通してしまう
法人番号のチェックとしてよく見るのが ^\d{13}$ です。これは「13桁あるか」しか見ていないので、1桁の打ち間違いや隣り合う数字の入れ替えをすべて通してしまいます。
法人番号は、12桁の基礎番号(商業登記法に基づく会社法人等番号)の前に、1桁の検査用数字(チェックディジット)を付けた番号です。国税庁が公開している計算方法は次のとおりです(国税庁「チェックデジットの計算」)。
- 12桁の基礎番号を、右端(最下位)を1桁目として数える
- 奇数桁の和と、偶数桁の和×2 を足す
- それを9で割った余りを、9から引いた値が検査用数字
\text{検査用数字} = 9 - \left( \sum_{n\,\text{奇数}} P_n + 2 \sum_{n\,\text{偶数}} P_n \right) \bmod 9
公式の計算例では、基礎番号 700110005901 の奇数桁の和が11、偶数桁の和が13で、11 + 13×2 = 37、37÷9 の余りが1、検査用数字は 9−1 = 8 となり、法人番号は 8700110005901 です。余りは0〜8なので、検査用数字は必ず1〜9になり、先頭が0の法人番号は存在しません。
| 入力値 | 内容 | ^\d{13}$ |
チェックディジット |
|---|---|---|---|
8700110005901 |
正しい(国税庁の計算例) | 通る | 一致 |
8700110005902 |
末尾を打ち間違い | 通る | 不一致(正しい検査用数字は7) |
8700110005910 |
末尾2桁を入れ替え | 通る | 不一致 |
0700110005901 |
先頭が0 | 通る | 不一致(先頭が0の法人番号は存在しない) |
実際に確かめると、正規表現(regex_ok)は4件すべてを通します。jpdq-oga の理由コード(error)は次のとおりです。
+----------------+------------------------+--------+-----------+
|corporate_number|memo |regex_ok|error |
+----------------+------------------------+--------+-----------+
|8700110005901 |正しい(国税庁の計算例)|true |NULL |
|8700110005902 |末尾を打ち間違い |true |check_digit|
|8700110005910 |末尾2桁を入れ替え |true |check_digit|
|0700110005901 |先頭が0 |true |check_digit|
+----------------+------------------------+--------+-----------+
ただし、チェックディジットも万能ではありません。 重み付き和を9で割った余りを使うため、隣り合う桁の差がちょうど9になる入れ替え(9 と 0 の入れ替え)は検出できません。たとえば 8700110005901 の「90」を「09」に入れ替えた 8700110005091 は、検査用数字が一致して通ってしまいます。チェックディジットは「多くの打ち間違いを見抜く仕組み」であって、「正しさの保証」ではありません。
落とし穴2:Pythonの \d とSparkの \d は意味が違う
Databricks のノートブックでは、pandas や Python で書いた検証と Spark で書いた検証が混ざりがちです。ここで同じ正規表現を使うと、全角数字の扱いが食い違います。
- Python の
reは、文字列パターンの\dが Unicode の数字全般(カテゴリ Nd)に一致します。全角の「8」も通ります(Python 公式ドキュメント)。 - Spark の
rlikeが使う Java の正規表現は、\dが既定で[0-9]のみです(Java の Pattern クラス)。
つまり 8700110005901(全角)は、pandas 側のチェックでは合格、Spark 側では不合格になります。どちらかに揃えるなら、Python 側は re.ASCII を付けるか、\d ではなく [0-9] と明示するのが安全です。
Python re.fullmatch(r'\d{13}') : True
Python re.fullmatch(r'\d{13}', re.ASCII) : False
+--------------------------+-----------+
|v |spark_rlike|
+--------------------------+-----------+
|8700110005901|false |
+--------------------------+-----------+
落とし穴3:数値として読むと番号が壊れる
法人番号は「数値」ではなく「番号(文字列)」として扱う必要があります。よくある壊れ方は2つです。
-
欠損が1件でもあると pandas が float にする:
pd.read_csvは空欄を含む数値列をfloat64にします。そのまま文字列にすると8700110005901.0のように末尾に.0が付き、Spark に渡した時点で全件が形式エラーになります。dtype=strを指定して読みましょう。 -
Excel を経由すると指数表記になる: Excel は一般形式の数字が11桁を超えると
8.70011E+12のように指数表記で表示し、15桁を超える桁は 0 に置き換えます。13桁の法人番号は15桁以内ですが、指数表記の文字列(8.70011E+12)のまま CSV などに保存されると、下の桁は元には戻せません。こうなった値が混ざる事故は運用でよく見られるので、取り込み時に「形式エラー」として検出し、発生元に戻す運用が必要です。
Spark で CSV を読むときも、inferSchema に任せると bigint になります。番号列はスキーマを明示して string で読むのが原則です。
pandas で空欄を含む CSV を読んでから Spark に渡すと、こうなりました。正しい法人番号が .0 付きになり、形式エラーになっています。
dtype: float64 / 値: [8700110005901.0, nan]
str に変換: ['8700110005901.0', nan] # pandas 3.0 では欠損は欠損のまま
dtype=str で: ['8700110005901', nan]
+----------------+------+
|corporate_number|error |
+----------------+------+
|8700110005901.0 |format|
+----------------+------+
なお、欠損の扱いは pandas のバージョンで変わります。pandas 2.x の astype(str) は欠損を 'nan' という文字列に変えるため、Spark 側の「NULL かどうか」のチェックもすり抜けます。pandas 3.0 では欠損のまま残りますが、.0 が付く問題は同じです。
和暦の落とし穴
落とし穴4:Sparkの日付関数は和暦を読めない
Spark の日時パターンで元号を表す文字 G は、公式ドキュメント上「AD; Anno Domini」(西暦紀元)しか想定していません(Spark Datetime Patterns)。そのため to_date("令和6年1月1日", "Gy年M月d日") のような書き方では和暦を日付にできません。Python の datetime.strptime にも和暦の元号を読む書式はありません。
-- ANSI モード有効
to_date('令和6年1月1日', 'Gy年M月d日')
-> DateTimeException: [CANNOT_PARSE_TIMESTAMP] Text '令和6年1月1日' could not be parsed at index 0.
-- ANSI モード無効
to_date('令和6年1月1日', 'Gy年M月d日') -> NULL
先頭の「令和」の時点で読めず、ANSI モードでは NULL ではなく例外になります。try_to_date のような「失敗したら NULL を返す」関数を使っても、NULL になるだけで和暦が読めるようにはなりません。
結局、「元号」「年」「月」「日」を自分で切り出し、元号ごとの開始年を足して西暦に直す実装が必要になります。ここで次の落とし穴にハマります。
落とし穴5:元号の境界に「存在しない日付」がある
「元号の開始年 + 年 − 1」で西暦に直すだけだと、元号の期間外の日付が正しい日付として通ってしまいます。たとえば「令和元年4月30日」は「2019年4月30日」に変換できますが、その日はまだ平成です。
各元号の開始日は、国立天文台暦計算室の「元号」に、詔書・政令の日付として載っています。
| 元号 | 開始日 | 根拠 |
|---|---|---|
| 大正 | 1912-07-30 | 明治45年7月30日の詔書「07月30日以後を大正元年とする」 |
| 昭和 | 1926-12-25 | 大正15年12月25日の詔書「12月25日以後を昭和元年とする」 |
| 平成 | 1989-01-08 | 昭和64年政令第1号「01月08日以後を平成元年とする」 |
| 令和 | 2019-05-01 | 平成31年政令第143号「05月01日以後を令和元年とする」(内閣府でも確認) |
この境界をまたぐ日付の判定は次のとおりです。
| 和暦 | 西暦にすると | 判定 | jpdq-oga の理由コード |
|---|---|---|---|
| 令和元年5月1日 | 2019-05-01 | 正しい(令和の初日) | NULL |
| 令和元年4月30日 | 2019-04-30 | 存在しない(まだ平成) | era_boundary |
| 平成31年4月30日 | 2019-04-30 | 正しい(平成の最終日) | NULL |
| 平成31年5月1日 | 2019-05-01 | 存在しない(もう令和) | era_boundary |
| 昭和64年1月7日 | 1989-01-07 | 正しい(昭和の最終日) | NULL |
| 昭和64年1月8日 | 1989-01-08 | 存在しない(もう平成) | era_boundary |
| 平成元年1月7日 | 1989-01-07 | 存在しない(まだ昭和) | era_boundary |
| 明治5年12月31日 | — | 太陽暦採用前 | pre_gregorian |
| 令和5年2月29日 | — | 2023年は平年 | not_exist |
jpdq-oga で実際に判定した結果です。元号の境界と、うるう年ではない年の2月29日を区別して検出できています。全角数字(令和6年1月1日)や漢数字(令和六年1月1日)も読み取れます。
+----------------+-------------+
|wareki |error |
+----------------+-------------+
|令和元年5月1日 |NULL |
|令和元年4月30日 |era_boundary |
|平成31年4月30日 |NULL |
|平成31年5月1日 |era_boundary |
|昭和64年1月7日 |NULL |
|昭和64年1月8日 |era_boundary |
|平成元年1月7日 |era_boundary |
|明治5年12月31日 |pre_gregorian|
|令和5年2月29日 |not_exist |
|令和6年1月1日|NULL |
|令和六年1月1日 |NULL |
+----------------+-------------+
明治の扱いにも注意が必要です。日本は明治5年12月3日を明治6年1月1日として太陽暦に切り替えたので(国立天文台暦計算室)、それ以前の日付は単純な西暦変換では扱えません。jpdq-oga は明治6年1月1日以降を対象にし、明治5年以前は「旧暦のため検証できない」という別の理由コード(pre_gregorian)で報告します。
なお実務では、改元直後に「平成31年5月」と書かれた書類が実際に存在します。これを「エラー」として弾くか、「警告」にとどめるかは、業務の運用で決めるのがよいと思います(DQX では criticality: warn で警告にできます)。
落とし穴6:ANSIモードでは存在しない日付でジョブごと止まる
Databricks Runtime 17.0 以降(Apache Spark 4.0 以降)では、ANSI モードが既定で有効です。ANSI モードでは、make_date や to_date、文字列から日付への CAST が、不正な値に対して NULL を返すのではなく例外を投げます(Databricks「ANSI compliance」)。
和暦を切り出して make_date(年, 月, 日) に渡す実装だと、「令和5年2月29日」のような値が1件混ざっただけで、品質チェックそのものが異常終了します。不正な値を見つけるための処理が、不正な値で止まるのは本末転倒です。
make_date(2023, 2, 29)
-> DateTimeException: Invalid date 'February 29' as '2023' is not a leap year.
to_date('2023-02-29')
-> DateTimeException: [CAST_INVALID_INPUT] The value '2023-02-29' of the type "STRING" cannot be cast to "DATE" ...
try_cast('2023-02-29' AS DATE)
-> NULL
make_date も to_date も例外になり、try_cast だけが NULL を返しました。対策は2つです。
-
try_castなど、失敗時に NULL を返すtry_系の関数を使う - そもそも日付を組み立てる前に、月日の実在チェック(うるう年を含む)を済ませる
jpdq-oga は後者です。月が1〜12で、日がその月の日数以内であることを確かめてからでないと、日付を組み立てません(SQL の CASE WHEN は条件を満たした枝だけを評価するため、不正な値では make_date が呼ばれません)。ANSI モードを無効にした場合と有効にした場合の両方で、同じ結果になることを自動テストで確認しています。
共通の落とし穴
落とし穴7:「組み込み関数で書けば速い」とは限らない
チェックディジットも和暦も、Python で書けば数行です。その関数を udf() で包むのが一番手軽ですが、「Python UDF は遅いので、組み込み関数で書き直すべき」というのがよく聞く定石です。
Databricks の公式ドキュメントも、Python UDF や pandas UDF は「データをシリアライズして JVM から Python インタープリターへ移す必要がある」ため遅くなりやすく、組み込み関数と SQL UDF が最も効率的だと説明しています(Databricks「What are user-defined functions (UDFs)?」)。
UDF が混ざっているかは、実行計画で確認できます。Python UDF を使うと、計画に BatchEvalPython(pandas UDF なら ArrowEvalPython)という演算子が現れます。
df.explain() # 実行計画に BatchEvalPython / ArrowEvalPython があれば Python への往復が発生している
定石どおりになるか、jpdq-oga の組み込み関数版と、pandas UDF 版を比べました。GitHub Actions(2 vCPU、local[*])で、1,000万行の合成データを処理した中央値です。全実装の true / false / NULL の件数が一致することを確認してから計測しています。
| チェック | 組み込み関数(jpdq-oga) | pandas UDF(python-stdnum) | pandas UDF(純 Python) | pandas UDF(numpy ベクトル化) |
|---|---|---|---|---|
| 法人番号 | 6.9 秒 | 29.1 秒 | 12.6 秒 | 5.2 秒 |
| 地方公共団体コード | 2.6 秒 | — | 11.4 秒 | — |
| 和暦 | 37.7 秒 | — | 38.6 秒 | — |
| 郵便番号の実在性(参照12万行) | 12.0 秒 | — |
3.7 秒(set) |
— |
結果は「定石どおり」ではありませんでした。
- 素朴に Python の関数を1行ずつ呼ぶ pandas UDF より速いのは、法人番号(1.8〜4.2倍)と団体コード(4.4倍)です。
- numpy で丁寧にベクトル化した pandas UDF は、法人番号では組み込み関数より約25%速い結果でした。チェックディジットのように算術に落とせる処理なら、pandas でも十分に速くなります。
- 和暦は同等でした。正規表現を段階的に当てる処理で、JVM でも Python でもコストが同程度です。
- 郵便番号の実在性は、
setを引くだけの pandas UDF のほうが約3倍速い結果でした。組み込み関数版は、有効期間つきの参照表を結合するぶん高価です。
しかも、計測の途中で、自分の実装の性能問題を3件見つけました。和暦の式が同じ部分式を何十回も評価しており、30万行に17.7秒かかっていたのが、1回の評価にまとめて1.1秒になりました。DQX 経由では、DQX が条件列を複数回評価するため、式の構造を変えて評価回数を減らしました。「組み込み関数で書いたから速い」ではなく、書き方次第でいくらでも遅くなります。文字種のチェックを書いたときも同じことが起きました。約 4,000 の範囲を並べた 1 つの正規表現の文字クラスで『変換できない文字』を探すと、200 万行(約 2,400 万文字)で約 350 秒かかりました。文字を 1 文字ずつ表で引く translate に替えると約 1 秒(手元の PC、2 スレッド)で、結果は同じでした。
「UDF は遅い、組み込み関数は速い」と決めつけず、自分の環境で実行計画と処理時間を測ることが大事です。組み込み関数で書く利点は、速度そのものよりも、Python のワーカーを介さず Spark の中で完結することや、式として他の処理に組み込みやすいことにあると考えています。
なお、計測値は実行ごとのばらつきが大きく(同じ実装で1.6倍の差が出ました)、単一マシンのローカルモードです。Photon やクラスタでの計測はしていません。数字は絶対値ではなく、同じ実行内の比率として読んでください。
OSS「jpdq-oga」を公開しました
ここまでの落とし穴をまとめて避けられるように、Databricks Labs のデータ品質フレームワーク「DQX」向けの拡張パッケージを公開しました。
- リポジトリ:
https://github.com/ogacloud2/jpdq-oga - ライセンス:MIT(DQX 自体は Databricks License です。本パッケージは DQX を同梱しません)
- Databricks・DQX の公式のプロジェクトではありません
特徴は4つです。
- 形式ではなく正しさを検証する: 法人番号は国税庁の計算式でチェックディジットを検証し、和暦は元号の期間と月日の実在まで確かめます。
- Python UDF を使わない: すべて Spark の組み込み関数で書いています。実行計画に Python の演算子が混ざらないことを、テストで確認しています。速度は落とし穴7のとおり、書き方と用途次第です。
- ANSI モードでも止まらない: 不正な値で例外を出さず、違反理由として報告します。
-
業務担当者が読める日本語のメッセージ:
lang: enで英語にも切り替えられます。
インストール
Databricks のノートブックで実行します。
%pip install databricks-labs-dqx==0.16.0 /Volumes/<カタログ>/<スキーマ>/<ボリューム>/jpdq-oga-0.1.0.zip
dbutils.library.restartPython()
リポジトリの Releases から v0.1.0 の ZIP(https://github.com/ogacloud2/jpdq-oga/archive/refs/tags/v0.1.0.zip)を取得し、展開せずに Databricks の Volume に置いて、そのパスを指定します。この方法は、Databricks Free Edition(サーバーレス)の実機で動作を確認しています。ZIP の URL を %pip install に直接指定する方法は、環境の外部通信の制限によって使えないことがあるため、実機では確認していません。main ではなく版(タグ)を指定すると、あとから内容が変わりません。画面つきの手順は、リポジトリの docs/databricks-free-edition.md にまとめました。
インストールしたら、最初に次を実行すると、環境(Spark・DQX・ANSI モード)と、全チェックの動作を 1 画面で確認できます。
import jpdq_oga
jpdq_oga.selfcheck() # すべて期待どおりなら True
使い方1:Spark の式として使う
DQX を使っていなくても、普通の PySpark の関数として使えます。正しい値は NULL、違反があれば理由のコードが返ります。
from jpdq_oga.expressions import corporate_number_problem, wareki_problem
df = spark.createDataFrame(
[("8700110005901", "令和元年5月1日"), ("8700110005902", "令和元年4月30日"), ("870011000590", "令和5年2月29日")],
"corporate_number string, contract_date string",
)
df.select(
"*",
corporate_number_problem("corporate_number").alias("corp_error"),
wareki_problem("contract_date").alias("date_error"),
).show(truncate=False)
+----------------+---------------+-----------+------------+
|corporate_number|contract_date |corp_error |date_error |
+----------------+---------------+-----------+------------+
|8700110005901 |令和元年5月1日 |NULL |NULL |
|8700110005902 |令和元年4月30日|check_digit|era_boundary|
|870011000590 |令和5年2月29日 |format |not_exist |
+----------------+---------------+-----------+------------+
| コード | 意味 |
|---|---|
format |
形式が違う(法人番号が13桁の半角数字でない、和暦として読めない) |
check_digit |
法人番号のチェックディジットが一致しない |
not_exist |
存在しない月日(2月30日、うるう年でない年の2月29日など) |
era_boundary |
その元号の期間外(令和元年4月30日など) |
pre_gregorian |
明治5年以前(旧暦のため検証できない) |
prefecture |
地方公共団体コードの先頭2桁が都道府県コード(01〜47)ではない |
使い方2:DQX の YAML ルールで使う
DQX を使っているなら、ルールの YAML に1行足すだけです。違反した行は、日本語の違反理由と一緒に隔離されます。
import yaml
from databricks.labs.dqx.engine import DQEngine
from databricks.sdk import WorkspaceClient
from jpdq_oga import get_japan_checks
customers = spark.createDataFrame(
[
("A001", "サンプル商事", "8700110005901", "令和元年5月1日"),
("A002", "テスト工業", "8700110005902", "平成30年4月1日"),
("A003", "デモ物産", "8700110005901", "令和元年4月30日"),
("A004", "例示電機", "870011000590", "昭和64年1月8日"),
("A005", "見本食品", "8700110005901", "令和5年2月29日"),
],
"customer_id string, name string, corporate_number string, contract_date string",
)
checks = yaml.safe_load("""
- criticality: error
check:
function: is_valid_jp_corporate_number
arguments:
column: corporate_number
- criticality: error
check:
function: is_valid_jp_wareki_date
arguments:
column: contract_date
""")
engine = DQEngine(WorkspaceClient())
valid_df, quarantine_df = engine.apply_checks_by_metadata_and_split(
customers, checks, custom_check_functions=get_japan_checks()
)
valid_df.show(truncate=False)
quarantine_df.selectExpr(
"customer_id", "corporate_number", "contract_date",
"transform(_errors, e -> e.message) AS errors",
).show(truncate=False)
-- 正常データ
+-----------+------------+----------------+--------------+
|customer_id|name |corporate_number|contract_date |
+-----------+------------+----------------+--------------+
|A001 |サンプル商事|8700110005901 |令和元年5月1日|
+-----------+------------+----------------+--------------+
-- 隔離データ
+-----------+----------------+------------------+----------------------------------------------------------------------------+
|customer_id|corporate_number|contract_date |errors |
+-----------+----------------+------------------+----------------------------------------------------------------------------+
|A002 |8700110005902 |平成30年4月1日 |[法人番号のチェックディジットが一致しません(入力値: 8700110005902)] |
|A003 |8700110005901 |令和元年4月30日 |[元号の期間外の日付です。改元日(昭和64年1月7日/平成31年4月30日 など)を確認してください(入力値: 令和元年4月30日)]|
|A004 |870011000590 |昭和64年1月8日 |[法人番号は半角数字13桁で指定してください(入力値: 870011000590), 元号の期間外の日付です。改元日(…)を確認してください(入力値: 昭和64年1月8日)]|
|A005 |8700110005901 |令和5年2月29日|[存在しない日付です(入力値: 令和5年2月29日)] |
+-----------+----------------+------------------+----------------------------------------------------------------------------+
5件のうち、正常データに残ったのは A001 だけでした。残りの4件は、日本語の違反理由と一緒に隔離されています。A002 は日付は正しいものの法人番号の打ち間違い、A004 は法人番号と日付の両方が違反で、理由が2つ並んでいます。全角数字の A005 も、「2023年はうるう年ではない」ことまで読み取って「存在しない日付」と判定できています。
(上の出力は、長いメッセージを見やすいように一部を省略して示しています。)
使い方3:時点を指定して参照データと照合する
郵便番号や地方公共団体コードのように「ある時点で存在したか」が問題になる値は、有効期間(valid_from / valid_to)つきの参照表と照合します。基準日(as_of)を変えると、同じデータでも結果が変わります。
-- 2026-01-01 時点
|id |postal_code|prefecture|メッセージ |
|1 |100-0001 |東京都 |NULL |
|2 |1000001 |大阪府 |郵便番号と都道府県が一致しません(基準日: 2026-01-01)(郵便番号: 1000001、都道府県: 大阪府)|
|3 |9999991 |東京都 |NULL |
|4 |9999992 |東京都 |郵便番号が日本郵便のデータに存在しません(基準日: 2026-01-01)(入力値: 9999992)|
-- 2024-03-31 時点(9999992 はまだ存在し、9999991 はまだ存在しない)
|3 |9999991 |東京都 |郵便番号が日本郵便のデータに存在しません(基準日: 2024-03-31)(入力値: 9999991)|
|4 |9999992 |東京都 |NULL |
(9999991・9999992 は説明用に作った架空の郵便番号です。)
日本郵便の郵便番号データ(再配布が認められています)と、総務省の全国地方公共団体コード(合併で廃止されたコードの履歴つき)を取り込む CLI も用意しています。ただし、履歴の粒度は取り込み頻度に等しく、平成17年4月1日より前に廃止されたコードは含まれません。
文字種のチェック
Shift_JIS のファイルへ出力する、旧システムへ連携するといった場面向けに、半角カナ、機種依存文字(①・㈱・髙など)、文字コードに変換できない文字(絵文字・𠮷など)を検出するチェックも入れています(has_no_jp_halfwidth_kana、has_no_jp_platform_dependent_chars、is_encodable_in_jp_charset)。判定は Python の cp932 / shift_jis の変換表に基づき、基本多言語面の全文字について、Spark の判定と一致することをテストで確認しています。列ごとの混入率は jpdq_oga.charset_profile(df, [...]) で出せます。「機種依存文字」の定義には幅があるため、本パッケージでは「cp932 で変換できて、shift_jis で変換できない文字」としています。実際の対象システムでの扱いは、別途確認してください。
Great Expectations でも使えます
DQX だけでなく、Great Expectations(GX 1.x)の期待としても使えます(pip install "jpdq-oga[gx]"、from jpdq_oga.gx_expectations import ExpectColumnValuesToBeValidJpCorporateNumber など、行チェック 7 種)。pandas と Spark の両方の実行エンジンに対応し、Spark では DQX 版と同じ組み込み関数の式を使います。確認したのは、GX 1.24.0 のローカル(pandas と Spark 3.5)で、Databricks 上での動作は未確認です。参照データを使うチェック(郵便番号・地方公共団体コードの実在性)は、GX 版には含まれません。
確認した範囲と、対象外
| 環境 | 状況 |
|---|---|
| Apache Spark 3.5(ANSI モードの無効・有効)、PySpark 4.0.0 | テストが通過 |
| Databricks Free Edition(サーバーレス) | ノートブックの部 0・A・B・C・D の動作を実機で確認 |
| Great Expectations 1.24.0(pandas・Spark 3.5、ローカル) | アダプタのテストが通過 |
| Databricks Runtime 17 以降の汎用クラスター・ジョブ、Photon、本番規模の件数、Databricks 上の Great Expectations | 確認していません(動作を保証しません) |
確認していない環境では、インストール後に jpdq_oga.selfcheck() を実行すると、環境と全チェックの動作を 1 画面で確認できます。
まとめ
| # | 落とし穴 | 対策 |
|---|---|---|
| 1 |
^\d{13}$ は打ち間違いを通す(チェックディジットも全てを見抜けるわけではない) |
国税庁の計算式でチェックディジットを検証する |
| 2 | Python と Spark で \d の意味が違う |
[0-9] と明示する、Python は re.ASCII を付ける |
| 3 | 数値として読むと番号が壊れる | 番号列は常に string で読む |
| 4 | Spark の日付関数は和暦を読めない | 元号・年・月・日を切り出して西暦に直す |
| 5 | 元号の境界に存在しない日付がある | 元号ごとの開始日・終了日と比較する |
| 6 | ANSI モードでは不正な日付で例外になる | 月日の実在を先に確かめる、try_ 系を使う |
| 7 | 「組み込み関数なら速い」とは限らない | 実行計画と処理時間を自分の環境で測る |
法人番号も和暦も、「それらしく見える」と「正しい」の間に差があります。社内データを AI や分析に使う前に、この差を自動で検出できるようにしておくと安心です。jpdq-oga へのフィードバックや Issue も歓迎します。
参考資料
- 国税庁 法人番号公表サイト「チェックデジットの計算」
- 国立天文台 暦計算室 暦Wiki:「元号」(詔書・政令の日付)/「月齢が生まれるまで」(明治5年12月3日の太陽暦への切り替え)
- 内閣府「元号について」(令和への改元の政令)
- 総務省「全国地方公共団体コード仕様」
- 日本郵便「郵便番号データの説明」
- Apache Spark「Datetime Patterns for Formatting and Parsing」
- Databricks「ANSI compliance in Databricks Runtime」
- Databricks「What are user-defined functions (UDFs)?」
- Python「re — 正規表現操作」
- Java「Pattern」(Java SE 17)
- databrickslabs/dqx(GitHub)