論文一覧に戻る 📚 用語集トップ 🗺 概念マップ
📚 用語解説
📚 用語解説
データ圧縮
Data Compression
データエンジニアリング

🔖 キーワード索引

可逆圧縮非可逆圧縮HuffmanZIPgzipJPEGPNGParquetSnappy圧縮率

別名・略称:(なし)

🔖 詳細キーワード索引

アルゴリズム 列指向 Python深掘り 落とし穴 情報理論 事例 歴史 FAQ

💡 30秒で分かる結論

🍰 まずはやさしく

データ圧縮は荷物をまとめる袋のようなものです。

データのサイズを小さくして保存しやすくします。

スマホで写真や動画を送る時に使われています。

この章では圧縮の種類と使い分けを学びます。

データ圧縮(Data Compression):ストレージ・通信量を減らす変換

💡 結論(補足)

データ圧縮はデータ基盤の費用・性能を左右する鍵。 「列指向+現代的可逆圧縮」が分析用途の標準。 一次的なシナリオごとに、 圧縮速度・解凍速度・圧縮率のトレードオフを意識すれば、 大半の問題は解決します。

📡 通信プロトコルにおける圧縮

ネットワーク経由のデータ転送でも圧縮は必須。 主要なケース:

プロトコル 圧縮方式 用途
HTTPgzip, brotli, zstdWeb ページ転送
gRPCgzip, snappyマイクロサービス通信
Kafkasnappy, lz4, zstdストリーミング
MQTTアプリ層で gzipIoT センサー
SSHzlibリモート操作

📍 あなたが今見ているもの

🍰 まずはやさしく

圧縮は大量のデータを扱うための必須技術です。

保存にかかるお金や時間を減らすために使います。

学校の課題ファイルをまとめて送る時に役立ちます。

データ基盤の中で圧縮がどう動くかを読みましょう。

大量のデータを扱うとき、 圧縮は避けて通れない要素技術。 SSDSE データのような数 MB の CSV なら無圧縮で良いですが、 数 GB を超える DWH のテーブルや log データは圧縮しないと ストレージ費・転送費・読み込み時間 が膨大になります。 Pandas で read_csv()read_parquet() を使う時、 知らないうちに圧縮が動いています。

📍 文脈 ── データ基盤の中で

データ圧縮は データレイク 設計、 分散処理、 ネットワーク転送、 メモリ効率の全てで効いてくる横断的技術。 「CPU を使ってでも、 ディスク IO / ネットワークを減らす」のが現代的トレードオフ。

分析パイプライン上では、 収集 直後の Bronze 層から最初に適用し、 Silver/Gold まで一貫させます。 列指向+圧縮で 10〜30 倍縮むのが普通で、 ストレージ費・クエリ費の双方を削減します。

🎨 直感で掴む

🍰 まずはやさしく

圧縮とは同じパターンを短く書き直すことです。

データの見た目を工夫して容量を減らします。

同じ文字が続くとき、回数で書く方法に似ています。

具体的にどうやって短くするのかを解説します。

圧縮形式の選び方

形式圧縮率速度用途
gzip汎用、 CSV/JSON
bzip2非常に高アーカイブ
LZ4非常に速リアルタイム処理
SnappyParquet標準
Zstd汎用、 推奨

🎨 直感で掴む(深掘り)

圧縮を一言で言えば「パターンを見つけて短く書く」。 「AAAAA」と書く代わりに「A×5」と書けば短くなる、 という発想です。

日本語にも例えれば「あいうえおあいうえおあいうえお」を「[あいうえお]×3」と表現できる。 アルゴリズムごとに「どんなパターンを探すか」が異なります:

📐 定義 / 数式

🍰 まずはやさしく

圧縮率とは元のサイズからどれだけ減ったかの割合です。

どのくらい効率よく小さくなったかを測ります。

買い物で割引率を計算する感覚に似ています。

圧縮の限界を決める理論的な仕組みについて読みます。

【圧縮率】
$$\text{圧縮率} = \frac{\text{圧縮後サイズ}}{\text{元のサイズ}}$$
小さいほど圧縮が効いている(例: 0.3 なら 70% 削減)
【シャノンのエントロピー】
$$H(X) = -\sum_{i} p_i \log_2 p_i \quad \text{(bit / symbol)}$$
圧縮の理論的下限。 これより小さくはできない。

📐 情報理論的限界(Shannon エントロピー)

どんなに優れた可逆圧縮でも、 Shannon エントロピー以下にはできません。 確率分布 $p(x)$ を持つ情報源の理論的下限:

$$ H(X) = -\sum_i p_i \log_2 p_i \quad [\text{bits/symbol}] $$

例:8 ビット文字でも、 ASCII 英文の実効エントロピーは約 4.7 bit/char。 つまり理論上 1.7 倍圧縮できる。 実際の gzip でも 2〜3 倍程度に収まります。

乱数や既に圧縮されたデータはエントロピーが高く、 さらなる圧縮はほぼ不可能。 「圧縮できる」=「データに冗長性がある」ということです。

📖 事例:センサーデータの圧縮戦略

工場で 1 kHz の振動センサーが 100 台稼働する場合、 1 日 8.64 億行。 CSV のままなら 30 GB/日、 月 1 TB。

📜 歴史

❓ よくある質問

Q1. 結局どの圧縮方式が最適?

「分析データ+頻繁にクエリ」なら Parquet + snappy。「アーカイブ用」なら Parquet + zstd(level 9) または xz。 ストリーミング系は lz4

Q2. CSV.gz vs Parquet、どちらが良い?

分析用途なら Parquet 一択。 列単位スキャン、 述語プッシュダウン、 統計情報、 全て備えていて、 多くは 5 倍以上速い。

Q3. 暗号化と併用するときの順序は?

圧縮 → 暗号化」の順。 暗号化済データはエントロピーが高く圧縮できない。 ただし圧縮による情報漏洩攻撃(CRIME, BREACH 等)に注意。

Q4. dtype 最適化はどこまで効果ある?

SSDSE のような中小データでメモリが半分以下、 ストレージも 30% 減程度。 数百万行を超えると違いが顕著。 categorical 化は特に効果が大きい。

Q5. 1 ファイル何 MB が理想?

分析向け Parquet では 100 MB〜1 GB が目安。 小さすぎるとメタデータ操作が重く、 大きすぎると並列度が落ちる。

📐 各圧縮アルゴリズムの仕組み

1. Huffman 符号 — 出現頻度に応じた可変長符号

頻出する文字には短い bit 列、 稀な文字には長い bit 列を割り当てる。 平均符号長 L は:

$$ L = \sum_i p_i \cdot \ell_i, \qquad H(X) \leq L < H(X) + 1 $$

数式を言葉で読み解く:平均符号長 L は、 Shannon エントロピー H(X) 以上、 H(X)+1 未満。 最適な符号長は文字頻度の負の対数 $-\log_2 p_i$ に近い。

2. LZ77 — 過去のパターンを参照

「N バイト前の M バイトをコピー」というポインタで繰返しを表現。 例:「ABABAB」→ 「AB」+「2 バイト前から 4 バイトコピー」。 gzip / DEFLATE はこの方式と Huffman の組み合わせ。

3. Burrows-Wheeler 変換 (BWT) — 並べ替えてから符号化

文字列を回転させて辞書順ソートし、 末尾文字列だけ取り出す。 同じ文字が隣り合いやすくなり、 後続の Move-to-Front + Huffman で圧縮率向上。 bzip2 の核心。

4. LZMA — 大きな辞書 + 範囲符号化

LZ77 の発展形。 辞書サイズが数 MB と大きく、 範囲符号化(算術符号化に類似)で各シンボルを実数値で表現。 xz / 7z で使用、 最高圧縮率と引き換えに展開も低速。

5. Snappy / LZ4 / Zstd — 速度優先

Google Snappy、 Facebook Zstandard、 Yann Collet の LZ4 は「圧縮率より展開速度」を優先する現代的圧縮。 Parquet / Avro / Hadoop / Kafka の標準として広く使われる。

🐍 Python 実装 #B1: Huffman 符号の手動構築

🎯 このコードでやること:SSDSE データのバイト頻度から Huffman 木を構築し、 各バイトの符号長を表示。

📥 入力データ:csv_bytes の各バイトの頻度カウント。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import collections
import heapq

# ── この抜粋だけで動くように、圧縮の対象になるバイト列を読み込む ──
# SSDSE-B-2026.csv をそのままバイト列として扱う(約 36 万バイト)
with open('data/raw/SSDSE-B-2026.csv', 'rb') as _f:
    csv_bytes = _f.read()
total = len(csv_bytes)
print(f'対象データ: {total:,} バイト')

freq = collections.Counter(csv_bytes)
heap = [[w, [byte, '']] for byte, w in freq.items()]
heapq.heapify(heap)

while len(heap) > 1:
    lo = heapq.heappop(heap)
    hi = heapq.heappop(heap)
    for pair in lo[1:]: pair[1] = '0' + pair[1]
    for pair in hi[1:]: pair[1] = '1' + pair[1]
    heapq.heappush(heap, [lo[0]+hi[0]] + lo[1:] + hi[1:])

codes = sorted(heap[0][1:], key=lambda x: (len(x[1]), x[0]))
print('頻出 10 バイトの Huffman 符号:')
for byte, code in codes[:10]:
    try: ch = chr(byte) if 32 <= byte < 127 else f'\\x{byte:02x}'
    except: ch = '?'
    print(f"  '{ch}' ({byte:>3}): freq={freq[byte]:>6}, code='{code}' ({len(code)} bits)")

avg_len = sum(freq[b]*len(c) for b,c in codes) / total
print(f'\n平均符号長: {avg_len:.3f} bits/byte')

📤 実行結果(イメージ)

対象データ: 359,821 バイト 頻出 10 バイトの Huffman 符号: ',' ( 44): freq= 62826, code='111' (3 bits) '0' ( 48): freq= 48599, code='101' (3 bits) '1' ( 49): freq= 40404, code='010' (3 bits) '2' ( 50): freq= 31876, code='000' (3 bits) '3' ( 51): freq= 27437, code='1101' (4 bits) '4' ( 52): freq= 25290, code='1100' (4 bits) '5' ( 53): freq= 24489, code='1001' (4 bits) '6' ( 54): freq= 22992, code='1000' (4 bits) '7' ( 55): freq= 22186, code='0111' (4 bits) '8' ( 56): freq= 21467, code='0011' (4 bits) 平均符号長: 3.633 bits/byte

💬 結果の読み方:「0」「,」「2」など SSDSE で頻出する数字記号には 3-4 bit の短い符号。 Huffman の平均符号長 3.60 は Shannon 下限 3.57 のわずか 1% 上 → ほぼ最適。

📐 Shannon エントロピー — 圧縮の理論限界

離散確率変数 $X$ について、 出力の不確実性を測る Shannon エントロピー:

$$ H(X) = -\sum_{i=1}^{n} p_i \log_2 p_i \quad \text{[bit/symbol]} $$

数式を言葉で読み解く:各シンボルの確率に「自身の負の対数」を掛けて足す。 確率が小さい(稀な)シンボルほど $-\log p$ は大きく、 確率 1/2 のシンボルは 1 bit 寄与する。

エントロピーの直感

Shannon の情報源符号化定理

ある情報源を可逆に符号化したとき、 平均符号長 $L$ は次を満たす:

$$ L \geq H(X) $$

つまり、 H(X) 未満には圧縮できない。 これが圧縮の理論的下限。 Huffman 符号は $L < H(X) + 1$ で「ほぼ最適」。

条件付きエントロピーとマルコフ情報源

「前のシンボルが分かっているとき」の不確実性:

$$ H(X_t \mid X_{t-1}) = -\sum_{i,j} p(x_i, x_j) \log_2 p(x_i \mid x_j) $$

数式を言葉で読み解く:直前のシンボルを観測した後の不確実性の平均。 通常 $H(X_t \mid X_{t-1}) \leq H(X_t)$ で、 「予測可能性が高い」テキストや CSV は条件付きエントロピーが大きく下がる。 LZ77 / LZMA はこれを活用。

🐍 Python 実装 #D1: マルコフ条件付きエントロピー

🎯 このコードでやること:SSDSE データのバイト列を 2 グラム(連続 2 バイト)の頻度から条件付きエントロピー H(X|X_prev) を計算し、 単純な H(X) と比較。

📥 入力データ:csv_bytes (358,876 bytes)。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from collections import Counter
import math

# 単純 H(X)
c1 = Counter(csv_bytes)
N = len(csv_bytes)
H1 = -sum((c/N)*math.log2(c/N) for c in c1.values())

# 2-gram 頻度
c2 = Counter(zip(csv_bytes[:-1], csv_bytes[1:]))
N2 = sum(c2.values())

# 条件付き H(X_t | X_{t-1})
prev = Counter(csv_bytes[:-1])
H_cond = 0
for (a,b), n_ab in c2.items():
    p_ab = n_ab / N2
    p_a  = prev[a] / N2
    H_cond -= p_ab * math.log2(n_ab/prev[a])

print(f'H(X)         = {H1:.3f} bit/byte')
print(f'H(X|X_prev)  = {H_cond:.3f} bit/byte')
print(f'削減量       = {H1-H_cond:.3f} bit/byte')

📤 実行結果

H(X) = 3.591 bit/byte H(X|X_prev) = 3.207 bit/byte 削減量 = 0.384 bit/byte

💬 結果の読み方:直前 1 バイトが分かれば、 不確実性が 3.57 → 3.20 と約 10% 削減。 SSDSE のテキストには「カンマの後は数字」「改行の後は数字」など強いパターンがあるため。 LZMA はさらに数百〜数千バイトの文脈を使うため、 Shannon 1 階下限を下回る。

📐 数式を言葉で読み解く — 主要関係式

情報量の定義(自己情報)

$$ I(x) = -\log_2 p(x) \quad \text{[bit]} $$

数式を言葉で読み解く:確率 p の事象が起きたときの「驚き」を bit で表す。 p=1/2 なら 1 bit、 p=1/256 なら 8 bit。 確率が小さい(稀な)事象ほど情報量が多い。

相互情報量(mutual information)

$$ I(X;Y) = \sum_{x,y} p(x,y) \log_2 \frac{p(x,y)}{p(x)p(y)} $$

数式を言葉で読み解く:X と Y が独立なら 0、 強く相関するほど大きい。 圧縮では「シンボル間の依存」をどれだけ活用できるかの上限を与える。

符号長の上限と Kraft 不等式

前置符号(prefix code)の符号長 $\ell_i$ は次を満たす:

$$ \sum_{i=1}^{n} 2^{-\ell_i} \leq 1 $$

数式を言葉で読み解く:各符号語の「占める空間」を 2 進木で見ると、 合計が 1 を超えてはいけない。 Huffman 符号はこの制約下で平均符号長を最小化する。

レート歪み関数(lossy 圧縮の理論)

$$ R(D) = \min_{p(y|x): E[d(X,Y)] \leq D} I(X;Y) $$

数式を言葉で読み解く:許容歪み D 以下で送るのに必要な最小ビット数 R(D)。 D=0 のとき R(0) = H(X)(lossless と一致)。 D 増えれば R 減る。 JPEG や MP3 はこの理論の応用。

コルモゴロフ複雑性(理論的究極)

$$ K(x) = \min \{ |p| : U(p) = x \} $$

数式を言葉で読み解く:万能チューリング機械 U に与えれば x を出力する最短プログラム p の長さ。 実用上計算不可能だが、 「データの本質的非冗長性」の理論的指標。 lossless 圧縮はこの近似を試みている。

🔬 記号・式を言葉で読み解く

可逆圧縮
完全に元に戻せる。 テキスト・プログラム・数値データ用。
非可逆圧縮
完全には戻せないが、 知覚的に問題ない範囲で大幅圧縮。 画像/音声/動画用。
Huffman 符号
出現頻度の高い記号に短い符号を割り当てる古典手法。
辞書式(LZ77/LZ78)
繰り返しパターンを「ここまで戻って何文字コピー」と表現。 gzip 内部で使用。
カラムナ形式
Parquet/ORC。 列毎にデータ型と分布が一様 → 圧縮率が良い。

🔬 専門用語の解説

エントロピー $H(X)$
情報量。 圧縮できる理論的下限。 単位は bit/symbol。
符号長 $L$
圧縮後の平均ビット数。 $L \geq H(X)$ がShannon の限界。
圧縮率
元サイズ / 圧縮後サイズ。 「2 倍圧縮」=半分の大きさ。
RLE
Run-Length Encoding。 同じ値の連続を「値×回数」で表現。
辞書符号化
頻出パターンを辞書 ID に置換。 「東京都」→ ID=12 等。
列指向
同じ列の値を連続配置して保存。 圧縮効率と分析効率に有利。
可逆/不可逆
元データを完全復元できるか否か。 分析用は可逆。
スキーマ
列名・型・制約の定義。 Parquet / Avro は内部に保持。

🎯 圧縮の設計指針(決定木)

  1. 用途は? ── 分析(読み多)/アーカイブ(保存多)/転送(速度重視)
  2. 分析なら ── 列指向必須(Parquet/ORC)、 圧縮は snappy or zstd(速)
  3. アーカイブなら ── 圧縮率重視、 xz / brotli / zstd level 19 等
  4. 転送なら ── lz4 / snappy(CPU負荷小)、 ネットワーク 1 GbE 未満なら zstd
  5. 更新が多いなら ── 列指向は不向き、 行指向(Avro)も検討
  6. SQL/BI で頻繁にクエリ ── Parquet+パーティション+snappy が王道
  7. ストリーミング ── 行指向 Avro+snappy がKafka 連携で便利

📊 圧縮ベンチマーク参考値

SSDSE-B 程度の表形式データを各方式で圧縮した場合の代表的な数値(実測値は環境依存):

形式 サイズ(CSV比) 読み速度 書き速度
CSV(基準)100%1.0×1.0×
CSV.gz25%0.8×0.3×
Parquet (snappy)15%
Parquet (zstd)10%
ORC (zstd)9%
Feather/Arrow35%10×10×

🛠 主要な圧縮アルゴリズム

圧縮は 可逆(lossless)不可逆(lossy) に大別されます。 データサイエンスでは可逆が基本:

アルゴリズム タイプ 圧縮率 速度 向く用途
gzip (DEFLATE)可逆汎用、Web
bzip2 (BWT)可逆アーカイブ
xz (LZMA2)可逆極高長期アーカイブ
zstd (Zstandard)可逆現代の汎用最強
snappy可逆極速Parquet, Spark
lz4可逆極速リアルタイム
brotli可逆Web(HTTP)
JPEG不可逆高(10〜100倍)写真
H.264/H.265不可逆極高動画

2025 年現在、 大量データのデフォルトは Parquet + zstd または Parquet + snappy。 zstd は近年急速にシェア拡大。

📊 列指向ストレージと圧縮の相乗効果

分析データは 列指向(columnar) 形式で保存すると圧縮率が劇的に向上します。 同じ列に並ぶ値は型と分布が似ているため、 アルゴリズムが効きやすくなるからです。

$$ \text{圧縮率} = \frac{\text{元サイズ}}{\text{圧縮後サイズ}} $$

技法 原理 効果
RLE (Run-Length)同値の連続を回数で表現繰り返し多い列で 100 倍
辞書符号化値を ID に置換カテゴリ列で 5〜20 倍
ビットパッキング最小ビット幅に圧縮整数列で 2〜4 倍
差分符号化 (Delta)隣接値との差を保存時系列で大幅縮小
FOR (Frame of Reference)基準値からのオフセット範囲狭い数値で効く
汎用圧縮(zstd等)を後段エンコード済み列をさらに圧縮最終的に 10〜30 倍

🔬 SSDSE-B-2026 を実際に圧縮して比較

SSDSE-B-2026 の生 CSV (約 359 KB) を、 gzip / bz2 / xz / Parquet の各形式で圧縮し、 サイズと展開時間を比較します。 「lossless 圧縮」の実態を体感するのが目的。

🧮 圧縮率の比較表(実測値)

形式サイズ (bytes)元比 (%)タイプ
CSV (cp932) 元ファイル359,821100.0%テキスト・無圧縮
CSV (utf-8 メモリ)358,87699.7%テキスト・無圧縮
gzip158,03344.0%DEFLATE (LZ77 + Huffman)
bz2141,19039.3%Burrows-Wheeler + Huffman
xz (lzma)128,27635.7%LZMA2 + 範囲符号化
Parquet (snappy)361,923100.8%列指向 + 軽量圧縮
Parquet (gzip)274,78476.6%列指向 + gzip
理論最小 (Shannon)159,90244.6%バイト単位エントロピー上限

観察:xz が最も圧縮率高(35.7%)、 Shannon 限界(44.6%)すら下回るのは「バイト単位でなく長い文字列の繰り返しを使う」LZMA の利点。 Parquet (snappy) は CSV より大きい(メタデータ + 列ごとのオーバーヘッド)が、 多数列・大規模なデータでは大幅に効くようになる。

🐍 Python 実装 #A1: 各種圧縮を実測

🎯 このコードでやること:SSDSE-B-2026 を読み、 gzip / bz2 / xz のメモリ内圧縮サイズを直接計測。

📥 入力データ:SSDSE-B-2026.csv (約 359 KB)、 564 行 × 112 列。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import pandas as pd
import gzip, bz2, lzma, os

df = pd.read_csv('data/raw/SSDSE-B-2026.csv',
                 encoding='cp932', skiprows=[1])
csv_bytes = df.to_csv(index=False).encode('utf-8')
orig = len(csv_bytes)

print(f'CSV (utf-8 in mem): {orig:>9,} bytes (100.0%)')
for name, fn in [('gzip', gzip.compress),
                  ('bz2', bz2.compress),
                  ('xz',  lzma.compress)]:
    comp = fn(csv_bytes)
    print(f'{name:>4}: {len(comp):>9,} bytes ({len(comp)/orig*100:5.1f}%)')

# raw file on disk
disk = os.path.getsize('data/raw/SSDSE-B-2026.csv')
print(f'\n元ファイル (cp932): {disk:,} bytes')

📤 実行結果

CSV (utf-8 in mem): 358,876 bytes (100.0%)
gzip: 158,033 bytes ( 44.0%)
bz2: 141,190 bytes ( 39.3%)
xz: 128,276 bytes ( 35.7%)

元ファイル (cp932): 359,821 bytes

💬 結果の読み方:xz は元の 3 割未満に圧縮。 SSDSE のような構造化数値テキストは、 列ヘッダの繰返し・整数の桁集中・空白パターンが多いため、 圧縮効率が高い。 一方、 ランダムバイト列ならどの圧縮も 95% 程度までしか小さくならない。

🐍 Python 実装 #A2: Shannon エントロピー

🎯 このコードでやること:バイト単位の出現頻度から Shannon エントロピー(bit/byte)を計算し、 「理論最小圧縮サイズ」を導く。

📥 入力データ:先ほどの csv_bytes(utf-8 でエンコードしたバイト列、 358,876 bytes)。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
import collections, math

counts = collections.Counter(csv_bytes)
total = len(csv_bytes)
entropy = -sum((c/total) * math.log2(c/total)
               for c in counts.values())

print(f'Shannon entropy = {entropy:.3f} bits/byte')
print(f'(理論上は最大 8.0 bits/byte = 完全ランダム)')
print(f'理論最小サイズ = {entropy * total / 8:,.0f} bytes')
print(f'実 gzip サイズ = 158,033 bytes')
print(f'実 xz サイズ   = 128,276 bytes (Shannon を下回る!)')

📤 実行結果

Shannon entropy = 3.565 bits/byte
(理論上は最大 8.0 bits/byte = 完全ランダム)
理論最小サイズ = 159,902 bytes
実 gzip サイズ = 158,033 bytes
実 xz サイズ = 128,276 bytes (Shannon を下回る!)

💬 結果の読み方:Shannon 限界(159,902 bytes)はあくまで「バイト単位の独立同分布」を仮定した下限。 xz は「複数バイトの繰り返しパターン」を辞書化してエントロピーを下げるため、 この限界を 下回ることが可能。 gzip は限界とほぼ同じ(159K vs 158K)。

🧮 実データで計算してみる

SSDSE-B-2026.csv(約 100 KB)を各形式で保存:

形式サイズ圧縮率
CSV(無圧縮)100 KB1.00
CSV.gz25 KB0.25
Parquet+Snappy18 KB0.18
Parquet+Zstd12 KB0.12

🧮 SSDSE 12 年パネルの圧縮戦略

SSDSE-B-2026 は 2012-2023 の 12 年分(564 行 = 47 県 × 12 年)。 全体を一度に保存するか、 年ごとに分割するかで圧縮効率が変わります。

🐍 Python 実装 #F1: 単一ファイル vs 分割保存

🎯 このコードでやること:SSDSE 全体を 1 ファイルに保存した場合と、 年ごとに 12 ファイルに分けて保存した場合の合計サイズを比較。

📥 入力データ:df 全件(564 行 × 112 列)。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
import io, gzip
import pandas as pd

all_df = pd.read_csv('data/raw/SSDSE-B-2026.csv',
                     encoding='cp932', skiprows=[1])

# 単一ファイル
buf_one = io.BytesIO()
all_df.to_csv(buf_one, index=False)
one_size = len(gzip.compress(buf_one.getvalue()))
print(f'単一 CSV gzip: {one_size:,} bytes')

# 年ごとに 12 個に分割
total_split = 0
for year in sorted(all_df['SSDSE-B-2026'].unique()):
    yr = all_df[all_df['SSDSE-B-2026']==year]
    buf = io.BytesIO()
    yr.to_csv(buf, index=False)
    sz = len(gzip.compress(buf.getvalue()))
    total_split += sz
print(f'年別 12 CSV gzip 合計: {total_split:,} bytes')
print(f'差: {total_split - one_size:+,} bytes')

📤 実行結果(イメージ)

単一 CSV gzip: 158,033 bytes 年別 12 CSV gzip 合計: 176,604 bytes 差: +18,571 bytes

💬 結果の読み方:分割すると圧縮効率が悪化(+25%)。 各ファイルが独自にヘッダ・辞書を持つため。 ただし「特定年だけ高速にアクセスしたい」なら分割が有利。 トレードオフ:圧縮率 vs アクセス性能。 多年データは Parquet パーティション (year=YYYY) が最適。

🐍 Python 実装 #F2: Parquet パーティション

🎯 このコードでやること:年ごとにパーティション化した Parquet を作成し、 特定年だけ高速読込できることを示す。

📥 入力データ:all_df 全件。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import os
os.makedirs('out', exist_ok=True)   # 書き出し先を先に作る
import os, glob
import pandas as pd
import pyarrow            # Parquet の読み書きに必要(ブラウザ版 Python には入っていない)

all_df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1])

# パーティション化保存
# to_parquet(partition_cols=...) は既存ディレクトリに *追記* する。
# 消さずに再実行するとファイルも行数も増え続けるので、毎回作り直す。
import shutil
shutil.rmtree('out/ssdse', ignore_errors=True)
all_df.to_parquet('out/ssdse', partition_cols=['SSDSE-B-2026'],
                  compression='zstd')

# 2023 年だけ読込(filter で高速化)
yr2023 = pd.read_parquet('out/ssdse',
                          filters=[('SSDSE-B-2026','=',2023)])
print(f'2023 年だけ: {len(yr2023)} 行')

# ファイル一覧
files = sorted(glob.glob('out/ssdse/**/*.parquet', recursive=True))
total = sum(os.path.getsize(f) for f in files)
print(f'\n総ファイル数: {len(files)}')
print(f'総サイズ: {total:,} bytes')

📤 実行結果(イメージ)

2023 年だけ: 47 行 総ファイル数: 12 総サイズ: 1,026,197 bytes

💬 結果の読み方:Parquet パーティションは年単位でファイルが分かれ、 filter 指定で必要パーティションだけ読み込む。 12 年データなら 1/12 の I/O で済む。 大規模データ(数十億行)では必須の最適化技法。

🧮 数式に値を入れて手で計算する: 圧縮率と節約容量

合成データで元 100MB → 各圧縮形式の節約容量を計算する。

Step 1: 形式別圧縮率

形式元 [MB]圧縮後 [MB]圧縮率節約
gzip100300.3070 MB
bzip2100220.2278 MB
xz100180.1882 MB
zstd100270.2773 MB

Step 2: 最良形式

xz 18 MB (82% 削減) が最良 gzip との差 = 30-18 = 12 MB 多く節約

🐍 Python で再現

1
2
3
4
5
6
7
8
import numpy as np
orig = 100
comp = np.array([30, 22, 18, 27])
save = orig - comp
ratio = comp / orig
print(f"圧縮率: {ratio}")
print(f"節約: {save} MB")
print(f"最良 index: {save.argmax()} ({save.max()} MB)")

📤 実行結果

圧縮率: [0.3 0.22 0.18 0.27] 節約: [70 78 82 73] MB 最良 index: 2 (82 MB)

💬 手計算 (Step 2) xz 82MB と Python 出力が完全一致。

🐍 Python 実装

SSDSE-B-2026(47 都道府県・2023 年データ)を題材にした最小コード:

📥 入力例(SSDSE-B-2026 全体:564 行 × 112 列 = 47 都道府県 × 2012〜2023 年) 年度 地域コード 都道府県 A1101(総人口) A1303(65歳以上人口) A4101(出生数) … 2023 R01000 北海道 5,092,000 1,681,000 24,430 … 2023 R13000 東京都 14,086,000 3,205,000 86,348 … 2023 R47000 沖縄県 1,468,000 350,000 12,549 … …(残り 112 列は住宅・家計・教育・医療など)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
import os
os.makedirs('data', exist_ok=True)   # 書き出し先を先に作る
import os
import pandas as pd
import pyarrow            # Parquet の読み書きに必要(ブラウザ版 Python には入っていない)

os.makedirs('data/processed', exist_ok=True)   # 保存先が無いと書き込みは失敗する

df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=1)

# gzip 圧縮で保存
df.to_csv('data/processed/ssdse.csv.gz', index=False, compression='gzip')

# Parquet(Snappy 圧縮、 デフォルト)
df.to_parquet('data/processed/ssdse.parquet', index=False)

# Parquet + Zstd(より高圧縮)
df.to_parquet('data/processed/ssdse_zstd.parquet', compression='zstd')

for _f in ('ssdse.csv.gz', 'ssdse.parquet', 'ssdse_zstd.parquet'):
    print(f'{_f:22s} {os.path.getsize("data/processed/" + _f):>10,} bytes')
📤 実行例(実測) ssdse.csv.gz 159,105 bytes ssdse.parquet 392,585 bytes ssdse_zstd.parquet 281,552 bytes

🐍 Python 実装(パフォーマンス比較)

(1) 各圧縮方式の比較

📥 入力例(SSDSE-B-2026 全体:564 行 × 112 列 = 47 都道府県 × 2012〜2023 年) 年度 地域コード 都道府県 A1101(総人口) A1303(65歳以上人口) A4101(出生数) … 2023 R01000 北海道 5,092,000 1,681,000 24,430 … 2023 R13000 東京都 14,086,000 3,205,000 86,348 … 2023 R47000 沖縄県 1,468,000 350,000 12,549 … …(残り 112 列は住宅・家計・教育・医療など)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import pandas as pd
import os, time
import pyarrow            # Parquet の読み書きに必要(ブラウザ版 Python には入っていない)

df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=1)

for comp in ['snappy', 'gzip', 'brotli', 'zstd', 'lz4']:
    t = time.time()
    df.to_parquet(f'/tmp/test_{comp}.parquet', compression=comp)
    sz = os.path.getsize(f'/tmp/test_{comp}.parquet')
    print(f'{comp}: {sz/1024:.1f} KB, {time.time()-t:.2f}s')
📤 実行例(実測) snappy: 383.8 KB, 0.01s gzip: 298.8 KB, 0.03s brotli: 269.7 KB, 0.02s zstd: 275.0 KB, 0.01s lz4: 384.1 KB, 0.00s

(2) DataFrame の dtype 最適化で圧縮率を上げる

📥 入力例(SSDSE-B-2026 の 2023 年・47 都道府県から 3 行) 都道府県 SSDSE-B-2026(年度) A1303(65歳以上人口) A1101(総人口) L3221(消費支出(二人以上の世帯)) Prefecture(都道府県) 北海道 2,023 1,681,000 5,092,000 296,888 北海道 東京都 2,023 3,205,000 14,086,000 341,320 東京都 沖縄県 2,023 350,000 1,468,000 251,222 沖縄県 …(全 47 行)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# ── この抜粋で使うデータを用意します(SSDSE-B の 47 都道府県・最新年度)──
import pandas as pd

df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', header=1)
df = df[df['地域コード'].astype(str).str.match(r'^R\d{5}$', na=False)].copy()
df['年度'] = pd.to_numeric(df['年度'], errors='coerce')
df = df[df['年度'] == df['年度'].max()]
for _c in df.columns[3:]:
    df[_c] = pd.to_numeric(df[_c], errors='coerce')
df['高齢化率'] = df['65歳以上人口'] / df['総人口'] * 100

# 見本でよく使われる仮の列名を、実データから作っておく
df['income'] = df['消費支出(二人以上の世帯)']
df['population'] = df['総人口']
_region = {'北海道': '北海道', '青森県': '東北', '岩手県': '東北', '宮城県': '東北',
           '秋田県': '東北', '山形県': '東北', '福島県': '東北', '茨城県': '関東',
           '栃木県': '関東', '群馬県': '関東', '埼玉県': '関東', '千葉県': '関東',
           '東京都': '関東', '神奈川県': '関東'}
df['region'] = df['都道府県'].map(_region).fillna('その他')
df['地域'] = df['region']

import numpy as np

# int64 を int32 へ
df['総人口'] = df['総人口'].astype('int32')

# object → category
df['都道府県'] = df['都道府県'].astype('category')

# float64 を float32 へ(精度が許せば)
df['高齢化率'] = df['高齢化率'].astype('float32')

print('メモリ使用量:', df.memory_usage(deep=True).sum() / 1024, 'KB')
📤 実行例(実測) メモリ使用量: 55.4462890625 KB

(3) ストリーミング圧縮(巨大ファイルでメモリ節約)

1
2
3
4
5
6
7
8
9
import gzip, shutil

# 大きな CSV を gzip 圧縮
with open('large.csv', 'rb') as f_in:
    with gzip.open('large.csv.gz', 'wb', compresslevel=6) as f_out:
        shutil.copyfileobj(f_in, f_out)

# pandas で直接読める
df = pd.read_csv('large.csv.gz', encoding='utf-8')

⚠️ よくある落とし穴

⚠️ 圧縮形式の互換性
Parquet+Snappy は環境によって読めない。 → Pandas+pyarrow が標準。
⚠️ 複数回圧縮しても効かない
圧縮済みファイルを再圧縮しても、 ほぼ縮まない。
⚠️ CPU と I/O のトレードオフ
高圧縮は CPU を食う。 ストリーミングなら LZ4/Snappy が良い。
⚠️ 非可逆圧縮の繰り返し
JPEG を編集→保存を繰り返すと劣化が累積。
⚠️ 暗号化との順序
「暗号化 → 圧縮」は意味なし。 「圧縮 → 暗号化」の順で。

⚠️ 落とし穴(さらに)

❌ 1. 不可逆圧縮の誤用
数値データに JPEG を当てれば情報が失われる。 分析用途は必ず可逆
❌ 2. 過度な圧縮でクエリ遅延
xz レベル 9 は圧縮率高いが解凍が遅い。 「読む頻度=高」なら snappy / zstd 程度に。
❌ 3. 圧縮済データの再圧縮
既に圧縮された画像/動画/Parquet を tar.gz するのは時間の無駄。 圧縮できない。
❌ 4. dtype を最適化しない
int64 のままだと圧縮効率も悪い。 int32/16/8 で十分な列が多い。 categorical を活用。
❌ 5. 圧縮形式のバージョン非互換
古い zstd で圧縮、新版でしか読めない、 等の問題。 標準的なライブラリと互換テスト。

⚠️ Lossless vs Lossy — どちらを選ぶか

Lossless(可逆圧縮)

Lossy(非可逆圧縮)

数値データの「lossy 化」

SSDSE のような整数データに lossy 圧縮を適用するのは原則 NG だが、 機械学習用の特徴量化済みデータでは:

⚠️ 圧縮率 vs 速度 — 実務での選択基準

アルゴリズム圧縮率圧縮速度展開速度推奨用途
LZ4★★★★★★★★★★リアルタイム、 メモリ内圧縮
Snappy★★★★★★★★★★Parquet / Hadoop デフォルト
Zstd (default)★★★★★★★★★★★★汎用、 最新標準(推奨)
gzip★★★★★★★★★★Web / Linux で最広範
Brotli★★★★★★★★★★Web フォント、 静的 HTML
bz2★★★★★★★★アーカイブ(やや遅い)
xz / lzma★★★★★★★★配布パッケージ、 長期保存
Zstd (max)★★★★★★★★★最強圧縮(圧縮 1 回・展開多数)

よくある選択基準

⚠️ 圧縮の落とし穴 — 実務で踏みがちな罠

  1. ランダムデータは圧縮不可:暗号化済みファイル、 乱数列、 すでに圧縮済みのバイト列はほぼ圧縮できない。 「JPEG を再 zip しても小さくならない」のはこのため。
  2. 小さいファイルは効率悪い:数 KB のファイルでは圧縮ヘッダ・テーブルのオーバーヘッドで逆に大きくなることも。 多数の小ファイルは tar+gzip でまとめる。
  3. 浮動小数点の精度:FP64 をそのままテキスト化すると重複が少なく圧縮率悪。 必要な精度に丸めてから保存(df.round(6) など)すると 30-50% 削減できることがある。
  4. 整数 vs 浮動小数:SSDSE のような整数主体データは圧縮率高。 浮動小数主体だと尾数部がランダム的で圧縮率低。 列指向ストレージ + 整数 quantize の組み合わせが有効。
  5. 圧縮チェーン:複数アルゴリズムを直列で適用しても、 2 回目以降はほぼ効果なし。 zip + zip より xz 単発の方が小さい。
  6. JIT/学習データの圧縮:機械学習のミニバッチ毎に展開する場合、 圧縮率より展開速度を優先(LZ4/Snappy)。
  7. ハードウェアアクセラレーション:Intel QAT, ZSTD ハードウェア実装などで高速化可能だが、 対応環境を要確認。
  8. セキュリティ — CRIME / BREACH 攻撃:HTTPS + gzip 圧縮で、 圧縮率の変化からトークンを推測する攻撃あり。 認証情報を含む応答は圧縮しない、 または別途防御策が必要。

🎮 触って理解する

圧縮の本質は「冗長さ(繰り返し・偏り)を見つけて短く書き直す」こと。ここでは 3 つのミニ実験で、(1) 繰り返しが多いほど縮む、(2) 頻出文字ほど短い符号が割り当てられる、(3) 非可逆圧縮は「誤差と引き換えに」縮む、という 3 原理を手を動かして確認します。

実験 1:ランレングス符号化(RLE)ライブデモ

テキストを自由に編集してください。「同じ文字が続く区間」を「文字+回数」に置き換えるのが RLE です。繰り返しが多いほど縮み、バラバラだと逆に膨らむ(A→A1 と 2 文字になる)ことを体感できます。

符号化結果:A4B3C2D1
10 文字 → 圧縮後 8 文字(圧縮率 80.0%縮んだ!
エントロピー 1.85 bit/文字(8 bit 固定と比べた理論限界の目安)
元サイズ 圧縮後 80.0%

実験 2:ハフマン符号 — 頻度で符号長が変わる

5 文字 A〜E の出現頻度をスライダーで変えると、ハフマン木がリアルタイムに組み替わります。頻出文字ほど木の浅い位置(短い符号)、稀な文字ほど深い位置(長い符号)に配置されるのが最適符号の原理です。平均符号長 L は必ず Shannon エントロピー H 以上、H+1 未満に収まります。

文字 頻度 符号 長さ
平均符号長 L = ? bit/文字 / エントロピー H = ? bit/文字(固定長なら 3 bit 必要)
100 文字書くと:固定長 300 bit → ハフマン ? bit(理論下限 ? bit)

実験 3:可逆 vs 非可逆 — 丸めの粗さと誤差のトレードオフ

滑らかな数値列(人工生成データ 64 点)を「刻み幅」で丸めると、値の種類が減ってエントロピー(=理論上の必要ビット数)が下がる代わりに、元の値との誤差(RMSE)が増えます。スライダーを右へ動かす(またはグラフ上を左右にドラッグする)と、非可逆圧縮の「圧縮率と情報損失のトレードオフ」を体感できます。

値の種類:? 種類 / エントロピー:? bit/値(元 ? bit/値)
理論サイズ比:?(エントロピー比) / 誤差 RMSE:?

💡 実験から読み取るべきこと

🗺 データ圧縮 概念マップ

データ圧縮 (data compression)
├── 可逆圧縮 (lossless)
│   ├── エントロピー符号化
│   │   ├── Huffman 符号 (1952)
│   │   ├── 算術符号 (1984)
│   │   └── 範囲符号化
│   ├── 辞書ベース
│   │   ├── LZ77 (1977)      ← gzip, DEFLATE
│   │   ├── LZ78 (1978)      ← UNIX compress, GIF
│   │   ├── LZW (1984)       ← TIFF
│   │   └── LZMA / LZMA2     ← xz, 7z
│   ├── BWT 系
│   │   └── BWT + MtF + RLE  ← bzip2
│   └── 速度優先
│       ├── Snappy           ← Hadoop, Parquet
│       ├── LZ4              ← Kafka, ClickHouse
│       └── Zstandard        ← Linux, Facebook
├── 非可逆圧縮 (lossy)
│   ├── 画像: JPEG / WebP / AVIF / HEIC
│   ├── 音声: MP3 / AAC / Opus
│   ├── 動画: H.264 / H.265 / AV1 / VP9
│   └── 数値: FP16, INT8, K-means 量子化
└── 列指向ストレージ (Parquet, ORC, Arrow)
    ├── 列単位圧縮(型ごとに最適化)
    ├── Dictionary encoding
    ├── Run-Length Encoding
    └── Bit-packing
  
データ圧縮 辞書符号化 エンコーディング バイナリ形式 列指向 DB ベクトル化(Arrow) 差分プライバシー

🔗 隣接手法への橋渡し

データ圧縮は情報理論と AI モデルの橋渡し。

形式選択 (CSV/Parquet) → 圧縮アルゴリズム選択 (gzip/snappy/zstd) → パーティション設計 → 検証の流れで、 圧縮率と速度の両立を測る。

🌳 手法選択フロー

データ圧縮は可逆/非可逆の判定 → アルゴリズム選択 → モデル圧縮への拡張で考える。

  1. 可逆圧縮が必要か? Yes → ハフマン符号、 No → LZ77 を先に確認
  2. 非可逆で十分か? Yes → JPEG、 No → MP3 を先に確認
  3. モデル圧縮も視野か? Yes → モデル圧縮、 No → 量子化 を先に確認

アーカイブなら gzip、 解析中間ファイルなら snappy、 列指向なら Parquet + zstd、 と「アクセス頻度と圧縮率」で選ぶ。

🧭 解説深化 — 圧縮を「ものさし」として使う(NCD と MDL)

ここまでのセクションは圧縮を「容量を減らす道具」として扱ってきた。この深化セクションでは視点を反転させ、圧縮後のサイズそのものを測定値として使うという、本ページの他の節では扱っていない使い方を紹介する。圧縮器は「データの規則性を探すエンジン」なので、その出力サイズは データの複雑さ・データ同士の類似度 の実用的な推定量になる。

💡 直感

圧縮サイズ C(x) は、計算不能なコルモゴロフ複雑性(このページの「数式」節参照)の実行可能な近似である。ここから面白い性質が出る: 2 つのデータ x, y を連結して圧縮したとき、共通パターンがあれば圧縮器は 2 回目の出現を「参照」で済ませられるので C(xy) は C(x)+C(y) よりずっと小さくなる。この「浮いた分」を正規化したのが正規化圧縮距離 NCD(Cilibrasi & Vitányi, 2005):

NCD(x, y) = ( C(xy) − min(C(x), C(y)) ) / max(C(x), C(y))

特徴量設計もモデルも距離関数の設計も不要で、「gzip や bz2 に突っ込むだけ」で任意のデータ間の類似度が出る。実際に SSDSE-B-2026.csv(2012–2023 年、47 都道府県パネル)から 7 都府県分の全 109 数値列 × 12 年をカンマ区切りテキスト化し、bz2 (level 9) で NCD を実測した結果(Python で実測・転記):

順位ペアNCD(bz2 実測)解釈
最小(最も似ている)鳥取県 – 島根県0.959隣接する山陰の小規模県ペア
2 位秋田県 – 島根県0.967人口減少の進む小規模県同士
3 位タイ秋田県 – 鳥取県 / 神奈川県 – 大阪府0.970小規模県同士・大都市圏同士
5 位東京都 – 神奈川県0.975首都圏ペア
最大(最も遠い)東京都 – 鳥取県0.988最大規模と最小規模の都県

ラベルも教師データも与えていないのに、「規模と構造が近い県ほど NCD が小さい」という妥当な順位が出た(比較 7 都府県: 東京・神奈川・大阪・秋田・青森・鳥取・島根、全 21 ペア中の抜粋)。数値が桁の並びとして共有されるほど連結圧縮が効く、という仕組みがそのまま類似度になっている。

再現コード(要点のみ):
📥 入力例(SSDSE-B-2026 全体:564 行 × 112 列 = 47 都道府県 × 2012〜2023 年) 年度 地域コード 都道府県 A1101(総人口) A1303(65歳以上人口) A4101(出生数) … 2023 R01000 北海道 5,092,000 1,681,000 24,430 … 2023 R13000 東京都 14,086,000 3,205,000 86,348 … 2023 R47000 沖縄県 1,468,000 350,000 12,549 … …(残り 112 列は住宅・家計・教育・医療など)
1
2
3
4
5
6
7
8
9
import pandas as pd, bz2
df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1])
cols = [c for c in df.columns if c not in ('SSDSE-B-2026','Code','Prefecture')]
def pref_bytes(p):
    sub = df[df['Prefecture']==p].sort_values('SSDSE-B-2026')
    return '\n'.join(','.join(str(r[c]) for c in cols) for _, r in sub.iterrows()).encode()
def C(b): return len(bz2.compress(b, 9))
def ncd(x, y): return (C(x+y) - min(C(x), C(y))) / max(C(x), C(y))
print(ncd(pref_bytes('鳥取県'), pref_bytes('島根県')))   # 0.959(実測)
📤 実行例(実測) 0.959106529209622

⚠️ 落とし穴(重要)

🚀 発展