ハッシュ差分列 (hashdiff column) とは、データの変更検知のために全カラムの値を連結してハッシュ化した列である。DWH への [[データインジェスチョン]] でハッシュ差分列を使用することで、レコードの重複排除や変更検知が高速化され、不要な処理コストを削減できる。
## 仕組み
| id | name | price | color |
| --- | ------ | ----- | ------ |
| 1 | apple | 100 | red |
| 2 | banana | null | yellow |
| 3 | cherry | null | null |
↓
| id | name | price | color | \_HASHDIFF |
| --- | ------ | ----- | ------ | ------------------------------ |
| 1 | apple | 100 | red | md5(1\|\|apple\|\|100\|\|red) |
| 2 | banana | null | yellow | md5(2\|\|banana\|\|\|\|yellow) |
| 3 | cherry | null | null | md5(3\|\|cherry\|\|\|\|) |
- 全てのカラムを連結する
- 区切り文字は `||` を使用する
- `null` は空文字に置換する
- ハッシュアルゴリズムは MD5 を使用する
> [!note]
> MD5 は暗号用途には不適切だが、データの変更検知には十分である。
```python
import hashlib
data = {
"id": [1, 2, 3],
"name": ["Apple", "Banana", "Cherry"],
"price": [100, None, None],
"color": ["Red", "Yellow", None],
}
```
## Polars DataFrame
```python
import polars as pl
df = pl.DataFrame(data)
df = df.with_columns(
pl.concat_str(pl.all().fill_null(""), separator="||")
.map_elements(lambda x: hashlib.md5(x.encode()).hexdigest())
.alias("_HASHDIFF")
)
```
| id | name | price | color | \_HASHDIFF |
| --- | ------ | ----- | ------ | -------------------------------- |
| 1 | apple | 100 | red | 62f8d5cec6ed61a77d4b80d93c1b9297 |
| 2 | banana | null | yellow | 557da1bd70a07b253ef6e5a8e10c8a41 |
| 3 | cherry | null | null | 863df792de43240f1b6fe1790b3f951a |
## pandas DataFrame
```python
import pandas as pd
df = pd.DataFrame(data, dtype=str)
df["_HASHDIFF"] = (
df.fillna("")
.astype(str)
.agg("||".join, axis=1)
.apply(lambda x: hashlib.md5(x.encode()).hexdigest())
)
```
| id | name | price | color | \_HASHDIFF |
| --- | ------ | ----- | ------ | -------------------------------- |
| 1 | apple | 100 | red | 62f8d5cec6ed61a77d4b80d93c1b9297 |
| 2 | banana | null | yellow | 557da1bd70a07b253ef6e5a8e10c8a41 |
| 3 | cherry | null | null | 863df792de43240f1b6fe1790b3f951a |
> [!note]
> pandas の DataFrame は、数値列に `null` が存在する場合、強制的に `float` 型に変換される。(e.g. `100` → `100.0`)
>
> この仕様によりハッシュ値が意図しない値になる可能性があるため、全て文字列として読み込む必要がある。
## 関連ノート
- **[前提]** [[Polars で全ての列を文字列型として DataFrame を作成する方法]]: 本ノートのハッシュ計算では、数値変換の丸め誤差で差分を取り違えないよう、元の形式 (文字列) のまま保持する必要がある