13. 不平等の測定
テーマ: ジニ係数・ローレンツ曲線・上位シェア・タイル指数
学習点: 累積和, 台形公式による数値積分, 複数指標の比較, テキスト描画
依存: 標準ライブラリのみ / 難易度: 中級
実行方法
uv run 13_gini_lorenz.py
スクリプト冒頭の PEP 723 メタデータ(# /// script)により、必要なライブラリは
uv が自動的に仮想環境へ導入します。事前の pip install は不要です。
解説
何をするプログラムか
所得や資産の「格差」を議論するには、分布の偏りを 1 つの数値に要約する指標が必要です。経済学で最も広く使われるのがジニ係数で、日本の再分配後所得のジニ係数は約 0.33、再分配前は 0.57 前後(厚生労働省・所得再分配調査)といった形で政策評価に用いられます。
このスクリプトは、完全に均等な社会・中程度の格差・極端な格差という 3 つの架空の所得分布(各 20 人)について、ジニ係数・上位シェア・タイル指数を計算して比較します。さらにローレンツ曲線を文字だけで描画し、累進課税と定額給付による再分配がジニ係数をどれだけ下げるかをシミュレーションします。すべて標準ライブラリのみの実装です。
コードの読みどころ
gini()は昇順ソート後の順位付き和の公式をそのまま実装しています。sum((i + 1) * v for i, v in enumerate(s))のように、enumerateで順位 i+1 を取りながらジェネレータ式で総和を計算します。lorenz()は累積和の典型例です。ループの中でc += vと累積し、(i / n, c / total)という(人口累積割合, 所得累積割合)のタプルを点列に追加していきます。enumerate(s, 1)で開始番号を 1 に指定している点にも注目してください。gini_from_lorenz()は台形公式による数値積分です。隣り合う 2 点で台形の面積(x_i - x_{i-1}) × (y_i + y_{i-1}) / 2を作って足し、G = 1 - 2Aで検算します。定義の異なる 2 つの実装が同じ値を返すことが正しさの確認になります。theil()ではfor x in xs if x > 0と条件付きのジェネレータ式を使い、log(0)の発生を防いでいます。draw()は 2 次元リストgridを文字のキャンバスに見立て、完全平等線(45 度線)を.、ローレンツ曲線の点を*として座標変換しながら書き込む、テキスト描画の実装例です。redistribute()はmain()の中で定義された関数で、閾値 1000 を超える所得にだけ課税し、税収をzip(xs, tax)で全員に定額給付として配り直します。
理論的背景
ローレンツ曲線 L(p) は「所得の低い方から p 割の人が全所得の何割を持つか」を表し、完全平等なら 45 度線に一致します。ジニ係数は 45 度線と曲線に挟まれた面積の 2 倍で、G = 1 - 2∫L(p)dp。0 が完全平等、1 に近いほど 1 人への集中を意味します。離散データでは昇順に並べた x について G = 2Σi·x_(i) / (nΣx) - (n+1)/n が成り立ち、gini() はこの式です。
タイル指数 T = (1/n)Σ(x_i/x̄)ln(x_i/x̄) は情報理論のエントロピーに由来する指標で、「地域内の格差+地域間の格差」へ加法的に分解できる点がジニ係数にない利点です。単一の指標では分布の形は捉えきれないため、上位シェアなど複数の指標を併記して比較しています。
実行結果の見方
先頭の表では、均等社会のジニ係数が 0.0000、格差社会が 0.8212 と、指標が分布の偏りに応じて動くことを確認できます。「検算(台形)」列が「ジニ係数」列と 4 桁まで一致しており、2 通りの実装の整合性が取れています。格差社会では上位 10% が全所得の 73.2% を保有しています。
ローレンツ曲線の図では、* の列が下に大きくたわみ、45 度線 . から離れるほど不平等が大きいことが視覚的に分かります。再分配シミュレーションでは、最高税率 20% でジニ係数が 0.8212 から 0.6716 へ低下し、税率 50% なら 0.4473 まで下がります。ただし末尾の注記のとおり、これは労働供給の変化(効率性の損失)を無視した計算であり、公平と効率のトレードオフを考える出発点にすぎません。
ソースコード
# /// script
# requires-python = ">=3.11"
# dependencies = []
# ///
"""13: 不平等の測定 -------------------------------------------------------
テーマ: ジニ係数・ローレンツ曲線・上位シェア・タイル指数
学習点: 累積和, 台形公式による数値積分, 複数指標の比較, テキスト描画
根拠: ジニ係数 G = 1 - 2∫L(p)dp(L はローレンツ曲線)。
離散データでは G = (2Σ i*x_(i)) / (n Σ x) - (n+1)/n (x は昇順)。
タイル指数 T = (1/n)Σ (x_i/x̄) ln(x_i/x̄) は加法分解可能な不平等指標。
"""
import math
def gini(xs) -> float:
"""離散データのジニ係数(昇順ソート後の公式)。"""
s = sorted(xs)
n = len(s)
total = sum(s)
if total == 0:
return 0.0
cum = sum((i + 1) * v for i, v in enumerate(s))
return (2 * cum) / (n * total) - (n + 1) / n
def lorenz(xs):
"""ローレンツ曲線の点列 (人口累積割合, 所得累積割合) を返す。"""
s = sorted(xs)
n, total = len(s), sum(s)
pts = [(0.0, 0.0)]
c = 0.0
for i, v in enumerate(s, 1):
c += v
pts.append((i / n, c / total))
return pts
def gini_from_lorenz(pts) -> float:
"""台形公式でローレンツ曲線下面積を求め G = 1 - 2A で計算(検算用)。"""
area = sum((pts[i][0] - pts[i - 1][0]) * (pts[i][1] + pts[i - 1][1]) / 2
for i in range(1, len(pts)))
return 1 - 2 * area
def theil(xs) -> float:
m = sum(xs) / len(xs)
return sum((x / m) * math.log(x / m) for x in xs if x > 0) / len(xs)
def top_share(xs, p: float) -> float:
s = sorted(xs, reverse=True)
k = max(1, round(len(s) * p))
return sum(s[:k]) / sum(s)
def draw(pts, width=46, height=18) -> None:
grid = [[" "] * (width + 1) for _ in range(height + 1)]
for r in range(height + 1): # 完全平等線
grid[r][int(round((height - r) / height * width))] = "."
for x, y in pts:
grid[height - int(round(y * height))][int(round(x * width))] = "*"
print(" 1.0 ┤" + "".join(grid[0]))
for r in range(1, height):
print(" " + "".join(grid[r]))
print(" 0.0 ┼" + "".join(grid[height]))
print(" 0.0" + " " * (width - 8) + "人口累積 1.0")
def main() -> None:
cases = {
"均等社会": [500] * 20,
"日本の所得(概念例)": [180, 220, 250, 280, 300, 320, 350, 370, 400, 420,
450, 480, 520, 560, 620, 700, 820, 1000, 1500, 3200],
"格差社会": [50, 60, 70, 80, 90, 100, 110, 130, 150, 180,
220, 300, 450, 700, 1200, 2000, 3500, 6000, 12000, 30000],
}
print(f"{'ケース':<20}{'ジニ係数':>10}{'検算(台形)':>12}"
f"{'上位10%':>10}{'上位1%':>9}{'タイル':>9}")
print("-" * 72)
for name, xs in cases.items():
print(f"{name:<20}{gini(xs):>10.4f}{gini_from_lorenz(lorenz(xs)):>12.4f}"
f"{top_share(xs, 0.10):>10.1%}{top_share(xs, 0.05):>9.1%}"
f"{theil(xs):>9.4f}")
print("\n[ローレンツ曲線] '.'=完全平等線, '*'=実際の分布(格差社会)")
draw(lorenz(cases["格差社会"]))
print("\n[再分配政策の効果シミュレーション] 累進課税+定額給付")
xs = cases["格差社会"]
def redistribute(xs, rate_top=0.45, threshold=1000, benefit_ratio=1.0):
tax = [max(0.0, (x - threshold)) * rate_top for x in xs]
pot = sum(tax) * benefit_ratio
transfer = pot / len(xs)
return [x - t + transfer for x, t in zip(xs, tax)]
print(f" 再分配前: ジニ = {gini(xs):.4f}")
for rt in [0.20, 0.35, 0.50]:
after = redistribute(xs, rate_top=rt)
print(f" 最高税率 {rt:.0%}: ジニ = {gini(after):.4f} "
f"({gini(after) - gini(xs):+.4f})")
print(" ※ ここでは労働供給への影響(効率性の損失)を無視した部分均衡の計算。")
if __name__ == "__main__":
main()
実行結果
ケース ジニ係数 検算(台形) 上位10% 上位1% タイル
------------------------------------------------------------------------
均等社会 0.0000 0.0000 10.0% 5.0% 0.0000
日本の所得(概念例) 0.4134 0.4134 36.3% 24.7% 0.3356
格差社会 0.8212 0.8212 73.2% 52.3% 1.4731
[ローレンツ曲線] '.'=完全平等線, '*'=実際の分布(格差社会)
1.0 ┤ *
.
.
.
.
.
.
.
.
. *
.
.
.
. *
.
. *
. *
. * * *
0.0 ┼* * * * * * * * * * * * *
0.0 人口累積 1.0
[再分配政策の効果シミュレーション] 累進課税+定額給付
再分配前: ジニ = 0.8212
最高税率 20%: ジニ = 0.6716 (-0.1496)
最高税率 35%: ジニ = 0.5595 (-0.2617)
最高税率 50%: ジニ = 0.4473 (-0.3739)
※ ここでは労働供給への影響(効率性の損失)を無視した部分均衡の計算。