02

結果を図で見る

都道府県の健康 — 豊かさと寿命の関連を男女別に比較
図を拡大して読む ↗ 凡例・数字の読み方

考察

豊かな県ほど男性の平均寿命が長い一方、人口当たりの病床・看護師・病院は少ないという関係が見られました。この2つの関係は、県を1つずつ除いても符号が変わりませんでした。豊かさと医療供給量が同じ方向に並ばないことから、地域の健康を考える際には、経済条件と医療の量を分けて捉える必要があります。医療供給量から寿命への効果は示せませんでしたが、供給量だけでは医療の質や必要性を表し切れないため、医療が役立たないという意味ではありません。

豊かさだけを使った補助回帰では、予測からのずれが男女で似ていました。これは、その回帰では捉えていない共通の地域要因を考える手掛かりですが、原因を特定した結果ではありません。また、女性で豊かさとの関連を検出できなかったことも、女性への効果がないことや、男女の効果が異なることの証明にはなりません。そこで、下記の追加検証では同じ標本内の係数差を直接比較しました。

分析の過程では、国と都道府県で相関が違う理由を集計単位の違いと解釈しかけました。しかし、所得の範囲をそろえると国でも相関が弱まり、その説明は撤回しました。比較する地域のばらつきが結論を左右するという教訓です。今後は過去の地域条件と後年の寿命を組み合わせ、生活習慣や医療へのアクセスなども事前に定義して検証することで、今回残った問いを絞り込めます。

分析と試行の記録を読む →

予測とのずれ

主要な国・地域の具体例

主要国・身近な地域をあらかじめ指定し、それに加えて各指標で残差の絶対値が最小の地域、正の残差が最大の地域、負の残差が最小の地域を機械的に選びました。同じ地域は1行にまとめています。残差の大小は記述的な比較であり、統計的な異常の判定ではありません。

選定した国・地域の予測との差。0を中心に左右で比較。健康は男女を併記。
地域名と予測との差を表示。クリックで原寸。正確な数値と選定理由は下表で確認できます。

平均寿命・男(年)

国・地域選定理由実測予測実測−予測平均予測の95%区間読み取り
東京都指定した比較例81.0781.86-0.79[81.33, 82.94]実測値は予測値を0.79年下回る。
大阪府指定した比較例80.2380.91-0.68[80.74, 81.18]実測値は予測値を0.68年下回る。
宮崎県予測に最も近い80.3480.34+0.00[80.05, 80.56]実測値は予測値を0.00年上回る。
長野県予測を最も上回る81.7580.58+1.17[80.42, 80.73]実測値は予測値を1.17年上回る。
青森県予測を最も下回る78.6780.36-1.69[80.09, 80.58]実測値は予測値を1.69年下回る。

平均寿命・女(年)

国・地域選定理由実測予測実測−予測平均予測の95%区間読み取り
東京都指定した比較例87.2687.24+0.02[86.78, 87.70]実測値は予測値を0.02年上回る。
大阪府指定した比較例86.7387.07-0.34[86.94, 87.20]実測値は予測値を0.34年下回る。
兵庫県予測に最も近い87.0787.07-0.00[86.94, 87.21]実測値は予測値を0.00年下回る。
長野県予測を最も上回る87.6787.01+0.66[86.89, 87.13]実測値は予測値を0.66年上回る。
青森県予測を最も下回る85.9386.97-1.04[86.79, 87.13]実測値は予測値を1.04年下回る。

この予測は豊かさのみを使う補助回帰です。予測に近くても健康の原因が説明されたわけではありません。東京は豊かさスコアが他県から離れており、回帰の傾きや予測がその位置に影響されます。生活習慣や医療へのアクセスなどは追加検証の候補ですが、個別の残差から原因を特定することはできません。

補助回帰の区間は合成指標を固定した600回の復元抽出による平均予測の区間です。個別の実測値の予測区間ではなく、区間の外にあるだけで異常とは判定できません。すべて標本内の当てはめです。

全地域の当てはまり・診断図とデータを見る

左は実測値と予測値(破線上で一致)、右は予測値と残差です。残差が正なら予測より高く、負なら低い値です。同じ標本に当てはめた結果であり、未知の地域や将来への予測精度ではありません。

上の具体例と同じ補助回帰の結果を、全地域について表示しています。CSVのmean_ci_low/mean_ci_highは上で説明した平均予測の95%区間で、前処理の不確実性は含みません。

平均寿命(男)の実測値・予測値と残差
平均寿命(男) — 全点表示。クリックで原寸
平均寿命(女)の実測値・予測値と残差
平均寿命(女) — 全点表示。クリックで原寸

表のregionは国・地域名、observedは実測値、predictedは予測値、residualは実測値−予測値です。地域名で検索できます。

  • 平均寿命(男):実測・予測・残差health_m_prediction_diagnostics.csv — クリックで表示
    regionobservedpredictedresidualmean_ci_lowmean_ci_high
    長野県81.7580.579587021447851.170412978552150280.4173705488693180.72760242472658
    滋賀県81.7880.8108035847730.969196415227003280.65911680409581.02238568301398
    熊本県81.2280.410607560892830.80939243910717180.1640528668461980.60992362031367
    大分県81.0880.451416493356650.62858350664335280.2284498324867380.63435701203
    福井県81.2780.720581619934250.549418380065745380.5758330937847180.8846269227676
    奈良県81.3680.839760475048420.520239524951577480.6854631257976781.06455387707334
    京都府81.480.980170998399560.419829001600447880.7823540472357881.30104861583023
    岡山県81.0380.619588940276230.410411059723770780.4660843400117880.7649994438593
    石川県81.0480.705494864520740.3345051354792616480.5629996279017280.86274694911334
    宮城県80.9980.66837634812510.321623651874901480.5233108636777680.81480177023852
    島根県80.7980.478256636765270.3117433632347399580.2664909229417780.65064612794652
    岐阜県81.080.709720897491850.2902791025081512580.5661117224265480.86890169678516
    佐賀県80.6580.380838642923480.26916135707652780.116657477356280.59206241836127
    神奈川県81.3281.087859620548040.2321403794519483280.8555405252152581.50307986317274
    香川県80.8580.622290974954610.2277090250453852680.4698794106544180.76760533494853
    新潟県80.6980.480193216950740.209806783049259680.2692239817630380.65183658872196
    広島県81.0880.883465148122080.1965348518779137580.7198563737608881.13212956393083
    三重県80.8680.702255563242770.1577444367572269480.5594630415356180.85868456216468
    静岡県80.9580.80429078842370.1457092115763032380.6524172128076381.01197045639078
    山梨県80.8580.71443866015540.1355613398445996280.570444718879580.87577259414167
    山形県80.5280.393802161195740.1261978388042592880.1351374537003580.59985491458053
    千葉県80.9680.889413216092070.070586783907927580.7266635928557981.14186654004683
    沖縄県80.2780.245787302633660.02421269736633746679.8815116753028980.50769759213321
    山口県80.5180.500473216203280.00952678379672988780.2989071784735380.66487251358588
    宮崎県80.3480.338156865857390.001843134142617941580.0476348544857980.56448144659318
    長崎県80.3880.39108972262923-0.01108972262923657680.130370948222180.59822979728267
    兵庫県80.9280.93383044496233-0.0138304449623234480.7536585226359781.22021394781481
    愛知県81.181.12207313442343-0.02207313442343661380.8801081886984681.56680802263703
    福岡県80.6680.69788583877717-0.0378858387771714480.5540322360600780.85464802636535
    埼玉県80.8280.85983545262697-0.0398354526269741980.6986417498977981.09428269690163
    富山県80.6180.69177727782457-0.0817772778245711280.5464403600664780.84614033938894
    群馬県80.6180.74189353650165-0.1318935365016500380.5988271477729880.91342052090317
    鳥取県80.1780.31289819729349-0.1428981972934906380.0029876730306680.5492736335747
    北海道80.2880.44763938336341-0.16763938336340880.223286228256780.63210360925157
    高知県80.2680.43051232413664-0.1705123241366379780.1957660259484980.62184926370222
    鹿児島県80.0280.33696662116188-0.316966621161881880.0455307295728580.5638260682575
    徳島県80.3280.67186759972822-0.35186759972822380.527862413207680.81728046676054
    愛媛県80.1680.55047864243376-0.3904786424337629680.3758790236244680.69823361356039
    福島県80.1280.51609787752719-0.396097877527182680.3272901947597880.67655813401717
    茨城県80.2880.71349526398079-0.433495263980788180.5694366020014380.87439864238857
    岩手県79.8680.38627292300814-0.526272923008136280.1231956576121980.59533374704617
    和歌山県79.9480.59231902654162-0.65231902654161980.434274115157280.7377650479112
    栃木県80.180.75949586217645-0.659495862176456280.6139382775097780.94000171587938
    大阪府80.2380.91007703576719-0.680077035767183780.74054537339981.1801731323388
    東京都81.0781.86244163512379-0.792441635123793781.3311757157165582.94433048145973
    秋田県79.5180.340542204687-0.830542204686992180.0518516771913580.56595098250283
    青森県78.6780.36287917699076-1.692879176990757680.0895529118336180.5796863239265
  • 平均寿命(女):実測・予測・残差health_f_prediction_diagnostics.csv — クリックで表示
    regionobservedpredictedresidualmean_ci_lowmean_ci_high
    長野県87.6787.00901951913360.660980480866399586.888078769355387.13044462680564
    滋賀県87.5787.050800502353350.51919949764663986.9413221607775287.1594923152674
    熊本県87.4986.978484820821350.511515179178644586.8174658811852387.13540054606216
    大分県87.3186.985859021432090.32414097856791186.8370719163156187.1285111970212
    福井県87.5487.034497334687640.505502665312363386.9276601901751287.13965491850485
    奈良県87.2587.05603303124010.1939669687598950586.942568466965487.17032292263123
    京都府87.3587.08140530403770.2685946959622924686.9440795111759787.2251019664007
    岡山県87.6787.01624789185610.653752108143905386.899451960404387.12653279670079
    石川県87.2887.03177114818220.2482288518177995286.9223181660052187.13593729998946
    宮城県87.1687.025063808152720.134936191847273886.9139935337198987.13232923737014
    島根県87.6486.990709052784710.649290947215291786.8480096236756487.12913866322603
    岐阜県86.8287.03253479508582-0.2125347950858298486.9249240725548387.13791428216601
    佐賀県87.1286.973105558002710.1468944419972899686.7999470527348887.13472047060496
    神奈川県87.2487.10086470802780.1391352919721953286.9317920880997187.26912349332382
    香川県87.2187.016736151278050.193263848721940786.8999704917216287.12656120601211
    新潟県87.3286.991058994082470.328941005917528286.8487802204603487.12918393640511
    広島県87.3387.063930494060490.266069505939512486.9448162182574387.18795504014165
    三重県86.9987.03118580433662-0.0411858043366208986.9203587145252187.13578753746386
    静岡県87.187.049623635821520.05037636417847579786.9426979909531887.1565246895029
    山梨県87.2287.033387297864380.186612702135619286.925629672752887.13869041744135
    山形県86.9686.97544807417721-0.01544807417721472186.8075852932974687.13471467114977
    千葉県86.9187.06500531380726-0.1550053138072655786.9441337325396187.19065074391271
    沖縄県87.4486.948701693066270.4912983069337286786.725692231114687.15084728801352
    山口県86.8886.99472360312379-0.1147236031237923786.8572412151215987.12963546839049
    宮崎県87.1286.96539293315550.1546070668445054386.7773634157920887.13550290839007
    長崎県86.9786.97495793476591-0.00495793476591188786.8062725550993387.13460396901843
    兵庫県87.0787.07303153592304-0.003031535923042838586.9425586271163887.2057636568258
    愛知県86.8687.10704711221442-0.2470471122144175586.932813923645287.2885864239379
    福岡県87.1487.030396192286570.109603807713426786.9192111685826387.13558551211457
    埼玉県86.6687.05966059273175-0.399660592731748986.9415661413044887.17729417096389
    富山県87.4287.029292371353560.390707628646438886.9189574959231587.13528483334336
    群馬県86.8487.03834841185822-0.1983484118582197286.9347395321451687.14282203966287
    鳥取県87.2786.960828675334850.3091713246651437486.7642990158462287.13975349899994
    北海道86.7786.98517649520224-0.2151764952022432586.8355445163875687.1284228961192
    高知県87.0186.98208162447210.0279183755279035586.8286186209072887.13200416292406
    鹿児島県86.7886.96517785516569-0.1851778551656906386.776572783709387.1357032056826
    徳島県86.6687.02569467958575-0.3656946795857578586.91574005281987.13237336201871
    愛媛県86.8287.00375961613335-0.183759616133357986.8765951385155787.12989624016163
    福島県86.486.99754698957673-0.597546989576727686.8632037821432687.1300864672564
    茨城県86.3387.03321682556268-0.703216825562677286.9255530379607587.13854229558373
    岩手県86.4486.97408753594479-0.534087535944792186.8038371619530787.13440738208342
    和歌山県86.4787.01132020072635-0.541320200726346586.8928538625220987.13098367981348
    栃木県86.2487.04152916354431-0.801529163544316286.9396369909887687.14598643367242
    大阪府86.7387.06873927944832-0.3387392794483190586.941811750454787.1973768811493
    東京都87.2687.240832181325970.0191678186740347286.7843297289507887.69734012719573
    秋田県86.3886.96582396543182-0.585823965431828286.7789479012463187.13510149772961
    青森県85.9386.96986027083997-1.039860270839966586.7914560011759487.13461630699985
  • 予測と残差の計算・図の生成prediction_diagnostics.py — クリックで表示
    """各分析の標本内当てはめと残差。SEMと補助回帰を明確に区別する。"""
    import numpy as np
    import pandas as pd
    import paths
    from make_path_diagram import text, esc, SURFACE, INK, INK3, C_POS, BOX_LINE
    
    
    def auxiliary(names, y, predictors):
        """固定した合成指標を使うOLS。区間は合成指標を固定した条件付き区間。"""
        x = np.column_stack([np.ones(len(y)), predictors])
        y = np.asarray(y, dtype=float)
        fitted = x @ np.linalg.lstsq(x, y, rcond=None)[0]
        rng = np.random.default_rng(2026)
        draws = []
        for _ in range(600):
            ix = rng.integers(0, len(y), len(y))
            draws.append(x @ np.linalg.lstsq(x[ix], y[ix], rcond=None)[0])
        lo, hi = np.quantile(draws, [.025, .975], axis=0)
        return pd.DataFrame(dict(region=list(names), observed=y, predicted=fitted,
                                 residual=y-fitted, mean_ci_low=lo, mean_ci_high=hi))
    
    
    def compute():
        from sem_country_v2 import prepare, HCR
        from sem_analysis import derive
        from pref_residuals import compute as pref_compute
        from job_residuals import compute as job_compute
        from muni_path import wide, V, fit
        z = prepare()
        gov = z[['rule_of_law', 'gov_effect', 'control_corrupt', 'reg_quality']].mean(axis=1)
        country = auxiliary(z.country, derive().loc[z.index, 'g_gdp'] * 100,
                            np.column_stack([gov, z[HCR].mean(axis=1), z.init_gdp]))
        p = pref_compute()
        j, _ = job_compute()
        datasets = [
            ('country', '国:一人当たりGDPの成長', '対数差×100', country,
             '補助回帰:制度・所得調整後人的資本の標準化平均と初期所得。SEMの潜在成長因子の予測ではない。'),
            ('health_m', '都道府県:平均寿命(男)', '年', auxiliary(p.pref, p.le_m, p[['wealth']]),
             '補助回帰:豊かさの3指標の標準化平均のみ。医療供給量を含むSEM全体の予測ではない。'),
            ('health_f', '都道府県:平均寿命(女)', '年', auxiliary(p.pref, p.le_f, p[['wealth']]),
             '補助回帰:豊かさの3指標の標準化平均のみ。医療供給量を含むSEM全体の予測ではない。'),
            ('job', '都道府県:転職率', '%(残差はポイント)', auxiliary(j.pref, j.jobchg, j[['urban','precar']]),
             '補助回帰:都市度・雇用の不安定さの指標の標準化平均。SEMの因子得点は使わない。'),
        ]
        w = wide()
        zz = (w[V] - w[V].mean()) / w[V].std()
        estimates = fit(zz).inspect()
        pred = np.zeros(len(w))
        for _, row in estimates[(estimates.op == '~') & (estimates.lval == 'move_rate')].iterrows():
            pred += row.Estimate * zz[row.rval].to_numpy()
        pred = pred * w.move_rate.std() + w.move_rate.mean()
        names = [f'{name} [{code}]' for code, name in zip(w.index, w['name'])]
        d = pd.DataFrame(dict(region=names, observed=w.move_rate.to_numpy(), predicted=pred,
                             residual=w.move_rate.to_numpy()-pred))
        datasets.append(('muni', '市区町村:総移動率', '%(残差はポイント)', d,
                         'パスモデルの総移動率の式:通勤流出率・単独世帯割合・高齢化率・人口密度の実測値から当てはめ。'))
        return datasets
    
    
    def build(title, unit, d, method):
        out = [f'<svg xmlns="http://www.w3.org/2000/svg" width="1280" height="690" viewBox="0 0 1280 690" role="img">',
               f'<title>{esc(title)}:実測・予測と残差</title>',
               f'<rect width="1280" height="690" fill="{SURFACE}"/>',
               text(40, 38, title + ' — 予測とのずれ', 22, INK, '700'),
               text(40, 66, f'N = {len(d):,} |単位:{unit}|全点を表示・各点は国/地域', 13, INK3),
               text(40, 92, method, 12, INK3)]
        for panel, ycol in enumerate(['observed','residual']):
            left, top, size = 92 + panel * 620, 154, 440
            x = d.predicted.to_numpy(); y = d[ycol].to_numpy()
            if panel == 0:
                low = min(x.min(), y.min()); high = max(x.max(), y.max())
                pad = (high-low)*.08 or 1
                xmin,ymin = low-pad,low-pad; xmax,ymax = high+pad,high+pad
            else:
                pad = np.ptp(x)*.08 or 1
                xmin,xmax = x.min()-pad,x.max()+pad
                edge=max(abs(y.min()),abs(y.max()))*1.08 or 1
                ymin,ymax=-edge,edge
            sx=lambda v:left+(v-xmin)/(xmax-xmin)*size
            sy=lambda v:top+size-(v-ymin)/(ymax-ymin)*size
            out.append(text(left, 128, '実測値と予測値(破線は一致線)' if panel==0 else '残差=実測値−予測値(破線は0)',15,INK,'600'))
            for v in np.linspace(xmin,xmax,5):
                out.append(f'<path d="M{sx(v):.2f},{top}v{size}" stroke="{BOX_LINE}"/>')
                out.append(text(sx(v), top+size+24, f'{v:.2f}',11,INK3,'400','middle'))
            for v in np.linspace(ymin,ymax,5):
                out.append(f'<path d="M{left},{sy(v):.2f}h{size}" stroke="{BOX_LINE}"/>')
                out.append(text(left-10,sy(v)+4,f'{v:.2f}',11,INK3,'400','end'))
            out.append(f'<path d="M{left},{top}v{size}h{size}" fill="none" stroke="{INK3}"/>')
            line = f'M{sx(xmin)},{sy(xmin)}L{sx(xmax)},{sy(xmax)}' if panel==0 else f'M{left},{sy(0)}h{size}'
            out.append(f'<path d="{line}" stroke="{INK3}" stroke-dasharray="5 4" fill="none"/>')
            for (_, r), xx, yy in zip(d.iterrows(), x, y):
                label=f'{r.region}:実測 {r.observed:.3f} / 予測 {r.predicted:.3f} / 残差 {r.residual:+.3f}'
                out.append(f'<circle cx="{sx(xx):.2f}" cy="{sy(yy):.2f}" r="3" fill="{C_POS}" opacity="0.6"><title>{esc(label)}</title></circle>')
            out.append(text(left+size/2,top+size+48,'予測値',13,INK,'400','middle'))
        out += [text(40,658,'同じデータで推定した標本内の当てはめ。未知の地域・将来に対する予測精度ではない。',12,INK3),
                text(40,679,'補助回帰の平均予測の95%区間はCSVに収録(600回・指標固定)。個々の実測値の予測区間ではない。' if 'mean_ci_low' in d else '地域名と全数値は付属CSVで検索可能。残差の大きさだけで原因や異常を断定しない。',12,INK3), '</svg>']
        return '\n'.join(out)
    
    
    def main():
        for key,title,unit,d,method in compute():
            assert np.isfinite(d.select_dtypes('number')).all().all()
            assert np.allclose(d.observed-d.predicted,d.residual)
            d.to_csv(paths.result(f'{key}_prediction_diagnostics.csv'),index=False)
            paths.figure(f'{key}_prediction_diagnostics.svg').write_text(build(title,unit,d,method))
            print(key, len(d))
    
    if __name__ == '__main__':
        main()
    

結果の確かさを確認する

区間が0を含む経路は、関連を明確に示せていません。以下の検証は、採用したモデルと前処理に条件づけられたものです。

ブートストラップによる各経路の判定
パス標準化係数ブートストラップ95%区間判定
経済的豊かさ → 入院医療キャパシティ-0.615[-0.778, -0.459]0を含まない
経済的豊かさ → 平均寿命(男)+0.545[+0.252, +0.930]0を含まない
入院医療キャパシティ → 平均寿命(男)+0.073[-0.259, +0.460]0をまたぐ
経済的豊かさ → 平均寿命(女)+0.272[-0.128, +0.636]0をまたぐ
入院医療キャパシティ → 平均寿命(女)+0.270[-0.082, +0.642]0をまたぐ

再抽出600回中、分散がほぼゼロの境界解は361回でした。境界解も含めた区間であり、モデルの安定性に注意が必要です。

追加検証では、豊かさの係数は男性側が大きく、医療供給量の係数の男女差は示せませんでした。比較の区間と1県抜きの検証は下に掲載しています。

推定の診断と追加検証を詳しく見る

追加検証:推定の健全性

復元抽出600回のうち、最適化の成功・係数と目的関数の有限性・負の分散なし・モデル共分散の正定値性という基本条件を満たしたのは600回、除外は0回でした。基本条件を満たしたうち、分散がほぼゼロの境界解は361回です。

境界の基準は分散の絶対値が10⁻⁸以下。境界解は収束失敗とは区別して記録し、一律には除外していません。これは局所最適性・情報行列・全てのモデル仮定を保証する検査ではありません。再抽出は既存と同じ前処理済みデータから行い、前処理やモデル選択の不確実性は含めていません。

男女差を直接比較する

同じ再抽出標本の中で、男性と女性の回帰係数の差を計算しました。主比較は寿命をどちらも年に戻した係数の「男−女」です。豊かさは県民所得の、医療供給量は看護師数の元標本の1標準偏差に尺度を合わせた潜在変数1単位当たりの差で、異なる因子間の比較には使いません。結果を見た後の追加検証であり、事前登録された検証ではありません。

説明側男−女(年)95%区間2比較を考慮した各97.5%区間読み取り
豊かさ+0.240[+0.074, +0.953][+0.049, +1.106]0を含まず、男性側が大きい
医療供給量-0.087[-0.184, +0.052][-0.200, +0.072]0を含むため、差を示せない

2本の年単位の比較にBonferroni対応の区間を併記しています。標準化係数の差は参考値としてCSVに収録。区間はブートストラップ近似で、境界解の多さやモデル選択の影響は残ります。女性側の効果がゼロという結論や、男女の因果効果の差を証明するものではありません。

豊かさの男女差:1県抜きの差の範囲は+0.199〜+0.675年(基本条件を満たす47県分)。境界解を除いた参考区間は[+0.137, +0.968](239反復)です。境界解の除外自体が標本を選別するため、主結果の代わりにはしません。

医療供給量の男女差:1県抜きの差の範囲は-0.109〜-0.020年(基本条件を満たす47県分)。境界解を除いた参考区間は[-0.152, +0.064](239反復)です。境界解の除外自体が標本を選別するため、主結果の代わりにはしません。

  • 男女の係数差と区間pref_gender_contrasts.csv — クリックで表示
    parameterscopeestimatevalidlowhighfamily_lowfamily_highzero_included
    WEALTH_yearsusable_including_boundary0.239615121640377776000.073784056132385440.95275368594366270.049423131430854561.1060000299786106False
    WEALTH_yearsinterior_only0.239615121640377772390.136970381364623440.96848122102101830.101163324531404681.115966484121967False
    WEALTH_stdusable_including_boundary0.254963536479364766000.03903116622754710.61656140713475430.015694310995864290.7007827231966391False
    WEALTH_stdinterior_only0.254963536479364762390.116131466015353360.71053007722341830.10613600763090340.7356364142750964False
    MED_yearsusable_including_boundary-0.08730098741996728600-0.184314977271807960.051612643733815036-0.200220239774330430.07206201103704195True
    MED_yearsinterior_only-0.08730098741996728239-0.151684209339700120.06425898833256682-0.185954981639628730.07047946534915403True
    MED_stdusable_including_boundary-0.2171418390991786600-0.41629139176832350.013212029840315909-0.43329979133318860.047367409541791285True
    MED_stdinterior_only-0.2171418390991786239-0.36941654484551120.05142364753018287-0.4279021802360550.08215622385405692True
  • 男女差の1県抜き診断pref_gender_contrast_loo.csv — クリックで表示
    excludedusableboundaryWEALTH_yearsWEALTH_stdMED_yearsMED_std
    北海道Truetax_income0.239370588212395540.2536771990729909-0.0880924237595856-0.2239714787094899
    青森県Truetax_income0.19860391921358910.2781510615245996-0.10897814107511311-0.2571081689189481
    岩手県Truetax_income0.23542287934791810.26791733150430697-0.08899113863167256-0.21367179278655912
    宮城県Truetax_income0.241268937403362830.2564935227730838-0.08452568175574678-0.21545056164993967
    秋田県Truetax_income0.22144031836476670.2610295950909877-0.09706535656964169-0.22596002091700262
    山形県Truetax_income0.243020380132271230.2564428966116698-0.08294134685172128-0.2127445901829317
    福島県Truetax_income0.242987999146529850.26808586808506574-0.08395275656840004-0.2063559127760515
    茨城県Truetax_income0.242259424183063450.2562936816260826-0.08207586574272117-0.1997003538039498
    栃木県Truetax_income0.242398822674558530.25971003563134354-0.08359917853010168-0.20302708907957837
    群馬県Truetax_income0.240508424663948730.25565453607594646-0.08612712777999555-0.21465149619480095
    埼玉県Truetax_income0.235474299214043670.25130778983778523-0.07924488701847093-0.18977533658072823
    千葉県Truetax_income0.23146951737777510.24890381388361144-0.0863128542392743-0.20801599959154488
    東京都True0.6748416849350960.40656451822410017-0.019618208699948056-0.11543667861339152
    神奈川県Truetax_income0.224056395304196080.26297306229842726-0.08880969673854092-0.21290523701476508
    新潟県Truetax_income0.227767212294567280.2238505502110148-0.09461916354119326-0.2444383135062011
    富山県Truetax_income0.236510394404834230.24922527618625903-0.08386470749090216-0.2120782848561794
    石川県Truetax_income0.238883770849163070.25657590695297877-0.08941613794561952-0.2186555417447234
    福井県Truetax_income0.240485167036531080.25556983913633036-0.08676901236208487-0.22154366520679925
    山梨県Truetax_income0.239263441553366460.250974328188882-0.08762483861995013-0.22207783610049228
    長野県Truetax_income0.251036727469501340.27450421729191354-0.07621327197710173-0.21624331319519607
    岐阜県Truetax_income0.251265205184013460.2733344713594691-0.07064663057990575-0.18678289175680715
    静岡県Truetax_income0.241938549498971920.2557114695690441-0.08525578022241465-0.21468800272150043
    愛知県Truetax_income0.23611223162508680.23339382167487366-0.08518175632781111-0.2096976844530154
    三重県Truetax_income0.2391959316767210.2552419160073725-0.08556295513781316-0.21256527042468884
    滋賀県Truetax_income0.24067818671035720.2723127124278174-0.07663771329729176-0.21116126210242575
    京都府Truetax_income0.226846458670387830.2534805833451216-0.09189474487076678-0.2226995496040704
    大阪府Truetax_income0.245620255345869620.26259344327134787-0.08679905694498476-0.21724406770883767
    兵庫県Truetax_income0.234303958719885190.2551375384872519-0.08676028145128109-0.21540925923045592
    奈良県Truetax_income0.221203414018593110.2502042295845147-0.08996645693666075-0.21996448467701443
    和歌山県Truetax_income0.239954075398127460.25806041428611304-0.08633951200418405-0.2235263801826152
    鳥取県Truetax_income0.2257707292406080.2286434956924731-0.0890527863923487-0.2191353728604908
    島根県Truetax_income0.228300118988613220.22288562074097057-0.08873969966959462-0.22856368670814042
    岡山県Truetax_income0.24113367098127330.2541662386646477-0.08499236886304315-0.21553723374161526
    広島県Truetax_income0.238807512685074220.2566721908187227-0.0884464816079414-0.21822348236577147
    山口県Truetax_income0.237820777453867120.2519701637156673-0.09166956559623167-0.2249504050966198
    徳島県Truetax_income0.235647915725726330.24314013667849466-0.09353666198503047-0.2405676912941444
    香川県Truetax_income0.238952821649632720.2544794706207143-0.0888009320267827-0.21800621121704503
    愛媛県Truetax_income0.240979177477119140.25792728802096343-0.08479145611862893-0.21487832975834262
    高知県Truetax_income0.23385612228732260.22757939429567037-0.08373307975659823-0.23502977179022444
    福岡県Truetax_income0.242205000434023630.2593836827429201-0.08417946652949122-0.20965912077731927
    佐賀県Truetax_income0.2382451178659920.2517355402742676-0.09582278834510378-0.22333606813887516
    長崎県Truetax_income0.239805363093539780.25199603026289075-0.08894347462006948-0.21918639793682182
    熊本県Truetax_income0.239705150153040340.25420872940913086-0.09997633581757154-0.22576864604336214
    大分県Truetax_income0.241659632527426670.2594770120058247-0.09710762767853004-0.2228782099788127
    宮崎県Truetax_income0.239463466031613170.25102036948678064-0.08701383713538303-0.20915754464022665
    鹿児島県Truetax_income0.242568374422992620.25833598923508866-0.08356119609994002-0.21420331449327307
    沖縄県Truetax_income0.211391622316992520.194151430223514-0.10709487074850116-0.2653804349379176
  • 全モデルの推定監査集計sem_audit_summary.csv — クリックで表示
    modelnattemptsusablefailednonconvergedboundaryusable_boundaryoriginal_usableoriginal_boundary
    country12860060000528528Truegov_effect
    pref-health4760060000361361Truetax_income
    pref-job4760060000378378True
    muni18594004000000True

全反復の監査記録をダウンロード

  • 推定監査と男女差の検証コードsem_audit.py — クリックで表示
    """推定の健全性と健康モデルの男女差の対比較。既存結果を残して監査する。"""
    import numpy as np
    import pandas as pd
    import paths
    
    TOL = 1e-8
    
    
    def diagnose(m):
        result = m.last_result
        e = m.inspect(std_est=True)
        numeric = e[['Estimate', 'Est. Std']].to_numpy(dtype=float)
        variances = e[(e.op == '~~') & (e.lval == e.rval)]
        finite = bool(np.isfinite(numeric).all() and np.isfinite(result.fun))
        negative = bool((variances.Estimate < -TOL).any())
        boundary = variances.loc[variances.Estimate.abs() <= TOL, 'lval'].tolist()
        sigma, _ = m.calc_sigma()
        eigmin = float(np.linalg.eigvalsh(sigma).min()) if np.isfinite(sigma).all() else np.nan
        usable = bool(result.success and finite and not negative and eigmin > 0)
        return e, dict(converged=bool(result.success), finite=finite,
                       negative_variance=negative, boundary=';'.join(boundary),
                       min_sigma_eigenvalue=eigmin, usable=usable,
                       optimizer_message=str(result.message))
    
    
    def contrast(e, outcome_sd):
        def coef(y,x,col):
            return float(e[(e.lval==y)&(e.rval==x)&(e.op=='~')][col].iloc[0])
        values = {}
        for x in ['WEALTH','MED']:
            # 共通の潜在変数の1単位当たり、男女とも年に戻して比較する。
            values[x+'_years'] = (coef('le_m',x,'Estimate')*outcome_sd['le_m']
                                 - coef('le_f',x,'Estimate')*outcome_sd['le_f'])
            values[x+'_std'] = coef('le_m',x,'Est. Std')-coef('le_f',x,'Est. Std')
        return values
    
    
    def main(n_override=None):
        import estat_sem as health
        import job_sem as job
        import sem_country_v2 as country
        import muni_path as muni
        hw = health.wide()
        hz = (hw[health.USED]-hw[health.USED].mean())/hw[health.USED].std()
        jw=job.wide(); jz=(jw[job.USED]-jw[job.USED].mean())/jw[job.USED].std()
        mw=muni.wide(); mz=(mw[muni.V]-mw[muni.V].mean())/mw[muni.V].std()
        configurations=[('country', country.prepare(), country.fit,600),
                        ('pref-health',hz,health.fit,600),('pref-job',jz,job.fit,600),
                        ('muni',mz,muni.fit,400)]
        records=[]; summaries=[]; contrast_draws=[]; original_contrast=None
        for name,z,fit,n in configurations:
            n=n_override or n
            rng=np.random.default_rng(0)
            for iteration in range(-1,n):
                sample=z if iteration==-1 else z.iloc[rng.integers(0,len(z),len(z))]
                row=dict(model=name,iteration=iteration,converged=False,finite=False,
                         negative_variance=False,boundary='',min_sigma_eigenvalue=np.nan,
                         usable=False,optimizer_message='')
                try:
                    e,flags=diagnose(fit(sample)); row.update(flags)
                    if name=='pref-health' and row['usable']:
                        values=contrast(e,hw[['le_m','le_f']].std())
                        if iteration==-1: original_contrast=values
                        else: contrast_draws.append(dict(iteration=iteration,boundary=row['boundary'],**values))
                except Exception as exc:
                    row['optimizer_message']=type(exc).__name__+': '+str(exc)
                records.append(row)
            subset=pd.DataFrame([r for r in records if r['model']==name and r['iteration']>=0])
            base=[r for r in records if r['model']==name and r['iteration']==-1][0]
            summaries.append(dict(model=name,n=len(z),attempts=n,usable=int(subset.usable.sum()),
                                  failed=int((~subset.usable).sum()),
                                  nonconverged=int((~subset.converged).sum()),
                                  boundary=int(subset.boundary.ne('').sum()),
                                  usable_boundary=int((subset.usable & subset.boundary.ne('')).sum()),
                                  original_usable=base['usable'],original_boundary=base['boundary']))
            print(summaries[-1],flush=True)
        pd.DataFrame(records).to_csv(paths.result('sem_audit_trials.csv'),index=False)
        pd.DataFrame(summaries).to_csv(paths.result('sem_audit_summary.csv'),index=False)
        d=pd.DataFrame(contrast_draws)
        if original_contrast is None or len(d)<100:
            raise RuntimeError('Insufficient valid health estimates for contrast intervals')
        d.to_csv(paths.result('pref_gender_contrast_draws.csv'),index=False)
        rows=[]
        for key,point in original_contrast.items():
            for scope,subset in [('usable_including_boundary',d),('interior_only',d[d.boundary==''])]:
                low,high=np.quantile(subset[key],[.025,.975]) if len(subset)>=100 else (np.nan,np.nan)
                # 主比較は同じ単位(年)の2本。Bonferroni対応の各97.5%区間。
                family_low,family_high=np.quantile(subset[key],[.0125,.9875]) if len(subset)>=100 else (np.nan,np.nan)
                rows.append(dict(parameter=key,scope=scope,estimate=point,valid=len(subset),
                                 low=low,high=high,family_low=family_low,family_high=family_high,
                                 zero_included=bool(low<=0<=high) if np.isfinite(low) else None))
        pd.DataFrame(rows).to_csv(paths.result('pref_gender_contrasts.csv'),index=False)
        loo=[]
        for index in hz.index:
            e,flags=diagnose(health.fit(hz.drop(index)))
            loo.append(dict(excluded=hw.loc[index,'pref'],usable=flags['usable'],
                            boundary=flags['boundary'],**contrast(e,hw[['le_m','le_f']].std())))
        pd.DataFrame(loo).to_csv(paths.result('pref_gender_contrast_loo.csv'),index=False)
        print(pd.DataFrame(rows).to_string(index=False))
    
    if __name__=='__main__':
        main()
    
豊かさから予測される寿命とのずれ(47都道府県)
豊かさから予測される寿命とのずれ(47都道府県) — figures/pref_residual_chart.svg

データと分析方法

e-Statの都道府県データを用い、経済的豊かさと入院医療キャパシティを潜在変数とするSEMで、男女別の平均寿命との関係を推定しました。小標本のため、係数の解釈ではブートストラップ区間と1県抜き診断を併せて確認します。

N = 47 χ²/df = 1.73 CFI = 0.968 TLI = 0.944 RMSEA = 0.126 df = 16

仮定した関係と根拠:関連と因果の違い

図の矢印は、モデルで仮定した関係の方向を示します。相関や関連があることと、一方を変えることで他方が変わる因果関係があることは別です。モデルがよく当てはまっても、因果関係が証明されたわけではありません。回帰パス係数は偏相関係数とは異なり、ここでは主に「関連」と表現しています。関連・相関・因果と係数の違い →

豊かさから医療供給量や寿命への矢印は、経済条件が医療資源や健康に関係するという仮説に基づきます。しかし、健康が経済活動を支える逆方向や、医療需要に応じて供給量が増える関係も考えられます。近接年の地域集計だけでは、これらを区別できません。

実際に、豊かさ→医療供給量を逆向きの矢印や双方向の共分散に置き換えた検算では、適合度がほぼ同じでした。この関連について、適合度は矢印の向きを選ぶ根拠になっていません。

したがって、仮説として方向を置いた地域間の関連として読みます。医療供給量の係数から病床を増減させた場合の寿命への効果を推定することはできません。豊かさや医療供給量から各指標への細い矢印も、指標が同じ概念を反映するという測定上の仮定です。

矢印の向きと等価モデルの検算記録

解釈上の注意

すべて観測データであり、矢印の向きはモデルの仮定にすぎない。同じ共分散行列を再現する等価モデルは無数にあり、向きを逆にしても適合度は変わらない(方法論 §5に検算あり)。区間が0をまたぐパスは「効果を示せなかった」ものとして扱うこと。図中の因子負荷のうち1本だけ有意性が示されないのは、その指標が潜在変数のスケールを決める参照指標として固定され推定されていないため(台帳に説明)。
データ・コード・再現方法

結果データ

  • 推定係数pref_estimates.csv — クリックで表示
    lvaloprvalEstimatestdsez-valuep
    MED~WEALTH-0.7279306194991939-0.60123966322775860.16939317741767446-4.297284168063221.729034786035477e-05
    income~WEALTH1.00.7880279915830273---
    tax_income~WEALTH1.26888844861501431.00.158448688793198768.008197847967751.1102230246251565e-15
    univ_rate~WEALTH1.06562821047234910.83978579621794810.157621446020024796.7606803348968591.3734569037637812e-11
    nurses~MED1.00.9541302819675782---
    beds~MED0.99903444325010180.9531716144146810.0686850388097535214.5451536542436980.0
    hosp~MED0.98753387090968650.94219935720688850.0714388637399805413.8234823343528710.0
    le_m~WEALTH0.63137468347636090.497568340178248970.2179637045754472.89669642339427740.0037711448201174225
    le_m~MED0.0270924594271222430.0258497514487431720.174024344152201920.155682008508369720.8762836793717501
    le_f~WEALTH0.30785909308756420.24260480369888420.231516618285145911.32974943814643080.18360084003371657
    le_f~MED0.2546838865131230.242991590547921770.19280459617910431.3209430250055630.18652036230538727
    MED~~MED0.56886166415686770.63851086736177150.132575188548865694.29086068350491.7798195217411106e-05
    WEALTH~~WEALTH0.60779013143695121.00.189396498540085943.2090885318299730.0013315648444991446
    le_m~~le_f0.70557218249220580.72094277050136430.159902408792688244.4125175337574811.021755169561267e-05
    le_m~~le_m0.75083513653060240.76722384785045250.1553835393527214.8321407766454121.3507268534773686e-06
    beds~~beds0.089517240455257580.091463873474110650.0298148263254248153.0024404460782870.0026782437028707307
    hosp~~hosp0.10987104155275540.112260371278926270.032331717885499463.39824323404212070.0006782009261603328
    income~~income0.37095666942507280.37901188448162030.082575997743019124.49230623376804157.045601298383275e-06
    le_f~~le_f0.932706515440850.95298526728884610.192851123515604884.8364069569993561.3220711312378342e-06
    nurses~~nurses0.087720803329490820.089635405032469610.0296111972052124222.96242001694842070.0030523109920437275
    tax_income~~tax_income0.00.00.049966910372971030.01.0
    univ_rate~~univ_rate0.28846710103937410.29475981647058690.069158595863433684.1710953994039173.0313891112188074e-05
  • 1県抜き診断pref_influence.csv — クリックで表示
    モデルパス全県LOO最小LOO最大最大変化その県
    健康経済的豊かさ → 平均寿命(男)0.497568340178248970.424646308263009750.66538076954704580.16781242936879687東京都
    健康経済的豊かさ → 平均寿命(女)0.24260480369888420.146495246738410130.3073061634525845-0.09610955696047407青森県
    健康経済的豊かさ → 入院医療キャパシティ-0.6012396632277586-0.6417597744581787-0.5649319706140841-0.0405201112304201高知県
    健康入院医療キャパシティ → 平均寿命(男)0.025849751448743172-0.070822476212556450.16470613704413450.13885638559539132東京都
    健康入院医療キャパシティ → 平均寿命(女)0.242991590547921770.186285692706391640.32759551459178380.08460392404386205高知県
    転職都市労働市場の厚み → 転職率0.49559423543998840.416288224208337830.61091265667377540.115318421233787沖縄県
    転職雇用の不安定さ → 転職率0.389668649626460860.161532092927130780.44160990790106525-0.22813655669933008沖縄県
  • 県別の寿命残差pref_residuals.csv — クリックで表示
    prefwealthincometax_incomeuniv_ratele_mle_m_hatle_m_resle_fle_f_hatle_f_res
    長野県-0.234660289438269142857.02893.548.981.7580.579587021447861.17041297855213687.6787.009019519133620.6609804808663853
    滋賀県0.51329764247419483110.03175.755.081.7880.810803584773010.96919641522698987.5787.050800502353370.5191994976466248
    熊本県-0.7812886366241082440.02747.546.181.2280.410607560892840.809392439107156787.4986.978484820821350.5115151791786445
    大分県-0.64927662031249212594.02768.746.481.0880.451416493356660.628583506643337887.3186.98585902143210.3241409785678968
    福井県0.221440375575131383082.02851.056.081.2780.720581619934260.549418380065731187.5487.034497334687660.5055026653123491
    奈良県0.6069697196539042658.03352.758.981.3680.839760475048440.520239524951563287.2587.056033031240120.19396696875988084
    京都府1.06118097145239322918.03266.466.481.480.980170998399570.4198290016004335787.3587.081405304037730.26859469596226404
    岡山県-0.105258865729133472816.02967.450.581.0380.619588940276240.4104110597237564787.6787.016247891856110.6537521081438911
    石川県0.17263652598895122954.02947.354.781.0480.705494864520760.3345051354792474387.2887.031771148182220.2482288518177853
    宮城県0.052562564332912493026.03045.349.580.9980.668376348125110.321623651874887287.1687.025063808152740.13493619184725958
    島根県-0.56245196611194172832.02677.547.080.7980.478256636765280.3117433632347257387.6486.990709052784720.6492909472152775
    岐阜県0.186307237272798222878.02987.955.181.080.709720897491860.2902791025081370486.8287.03253479508585-0.21253479508585826
    佐賀県-0.87758752628590442620.02696.243.080.6580.38083864292350.269161357076512887.1286.973105558002710.14689444199728996
    神奈川県1.40954078596714473099.03749.661.581.3281.087859620548060.232140379451934187.2487.100864708027830.1391352919721669
    香川県-0.096518106672236692884.02928.950.680.8580.622290974954610.2277090250453852687.2187.016736151278070.19326384872192648
    新潟県-0.55618736080136132835.02722.346.280.6980.480193216950750.2098067830492453887.3286.991058994082480.32894100591751396
    広島県0.74834911054605833206.03103.559.981.0880.88346514812210.1965348518778995487.3387.06393049406050.2660695059394982
    三重県0.162157773732342162971.03138.450.580.8680.702255563242790.1577444367572127286.9987.03118580433663-0.0411858043366351
    静岡県0.49222952512858383359.03101.253.080.9580.804290788423730.145709211576274887.187.049623635821530.050376364178461586
    山梨県0.201568635307342562885.02938.256.380.8580.714438660155410.135561339844585487.2287.03338729786440.186612702135605
    山形県-0.83565209494057962644.02632.344.880.5280.393802161195750.1261978388042450786.9686.97544807417722-0.015448074177228932
    千葉県0.76759039911866543033.03427.056.080.9680.889413216092080.0705867839079132986.9187.06500531380728-0.15500531380727978
    沖縄県-1.31446246176850232225.02696.739.280.2780.245787302633670.02421269736632325587.4486.948701693066280.49129830693371446
    山口県-0.49058398843921472958.02888.342.780.5180.500473216203290.00952678379671567786.8886.9947236031238-0.11472360312380658
    宮崎県-1.0156579709184342340.02625.945.180.3480.338156865857390.001843134142617941587.1286.965392933155510.15460706684449121
    長崎県-0.84442650933143482503.02721.144.680.3880.39108972262923-0.01108972262923657686.9786.97495793476591-0.004957934765911887
    兵庫県0.91127482280053442926.03404.660.680.9280.93383044496234-0.01383044496233765187.0787.07303153592306-0.00303153592307126
    愛知県1.52021741187007043836.03544.458.781.181.12207313442346-0.02207313442346503586.8687.10704711221445-0.24704711221444597
    福岡県0.148022237647374522770.03052.054.480.6680.69788583877718-0.03788583877718565487.1487.030396192286590.1096038077134125
    埼玉県0.67190987141858652977.03320.756.980.8280.85983545262698-0.039835452626988486.6687.05966059273176-0.39966059273176313
    富山県0.12826177346886553231.02867.552.080.6180.69177727782458-0.0817772778245853387.4287.029292371353580.3907076286464246
    群馬県0.29038187704721263219.03006.652.680.6180.74189353650166-0.1318935365016642486.8487.03834841185825-0.19834841185824814
    鳥取県-1.09736674311975162334.02628.843.580.1780.3128981972935-0.1428981972935048487.2786.960828675334850.30917132466514374
    北海道-0.66149511944819772700.02847.343.380.2880.44763938336342-0.167639383363422286.7786.98517649520224-0.21517649520224325
    高知県-0.71689910787693162537.02686.047.480.2680.43051232413666-0.1705123241366521887.0186.982081624472120.02791837552788934
    鹿児島県-1.01950827017086982407.02701.742.780.0280.33696662116188-0.316966621161881886.7886.9651778551657-0.18517785516570484
    徳島県0.063856345761462943074.02915.751.780.3280.67186759972823-0.351867599728237286.6687.02569467958577-0.36569467958577206
    愛媛県-0.32882241457152812516.02844.352.280.1680.55047864243377-0.3904786424337771786.8287.00375961613337-0.1837596161333721
    福島県-0.440040077559312352844.02858.045.780.1280.5160978775272-0.3960978775271968386.486.99754698957675-0.5975469895767418
    茨城県0.198516861500188683105.03091.650.580.2880.7134952639808-0.433495263980802386.3387.03321682556269-0.7032168255626914
    岩手県-0.86000828008009672613.02657.544.279.8680.38627292300815-0.526272923008150486.4486.97408753594479-0.5340875359447921
    和歌山県-0.193473775539410842805.02936.649.579.9480.59231902654163-0.652319026541633286.4787.01132020072635-0.5413202007263465
    栃木県0.347323295599733353302.03042.352.080.180.75949586217646-0.659495862176470486.2487.04152916354433-0.8015291635443305
    大阪府0.83443538464800742950.03318.060.580.2380.9100770357672-0.680077035767197986.7387.06873927944835-0.3387392794483475
    東京都3.91522097335038445858.04288.866.581.0781.86244163512382-0.792441635123822287.2687.240832181326010.019167818673992087
    秋田県-1.0079416850595972466.02580.744.679.5180.34054220468701-0.830542204687006386.3886.96582396543184-0.5858239654318425
    青森県-0.93568425086353162618.02609.143.678.6780.36287917699077-1.692879176990771885.9386.96986027083999-1.0398602708399807
  • 分析サンプルpref_sample.csv — クリックで表示
    pref_cdprefincometax_incomeuniv_ratenursesbedshosple_mle_fdoctors
    01北海道2700.02847.343.31236.61526.29.280.2886.77238.2
    02青森県2618.02609.143.61081.91119.76.278.6785.93197.9
    03岩手県2613.02657.544.21018.81083.45.979.8686.44193.8
    04宮城県3026.03045.349.5899.1867.34.980.9987.16231.7
    05秋田県2466.02580.744.61068.01162.95.379.5186.38223.2
    06山形県2644.02632.344.81002.61046.64.980.5286.96219.3
    07福島県2844.02858.045.7962.31047.45.480.1286.4195.5
    08茨城県3105.03091.650.5739.6940.25.580.2886.33180.1
    09栃木県3302.03042.352.0881.2906.74.680.186.24217.6
    10群馬県3219.03006.652.61018.21073.85.980.6186.84225.0
    11埼玉県2977.03320.756.9665.5695.34.080.8286.66160.1
    12千葉県3033.03427.056.0700.9790.24.080.9686.91189.7
    13東京都5858.04288.866.5746.8857.04.481.0787.26303.7
    14神奈川県3099.03749.661.5666.2681.33.281.3287.24205.2
    15新潟県2835.02722.346.2918.61031.24.980.6987.32191.9
    16富山県3231.02867.552.01144.21359.58.380.6187.42241.8
    17石川県2954.02947.354.71185.31329.17.381.0487.28280.6
    18福井県3082.02851.056.01115.51206.47.681.2787.54245.5
    19山梨県2885.02938.256.3922.71065.56.280.8587.22231.5
    20長野県2857.02893.548.9986.11032.05.581.7587.67225.9
    21岐阜県2878.02987.955.1914.9854.34.481.086.82208.6
    22静岡県3359.03101.253.0812.6874.74.080.9587.1200.7
    23愛知県3836.03544.458.7817.9773.23.881.186.86207.7
    24三重県2971.03138.450.5864.3926.74.880.8686.99216.9
    25滋賀県3110.03175.755.0848.5901.23.581.7887.57220.7
    26京都府2918.03266.466.4999.21252.76.281.487.35314.5
    27大阪府2950.03318.060.5912.61058.55.680.2386.73270.2
    28兵庫県2926.03404.660.6935.9993.25.880.9287.07242.2
    29奈良県2658.03352.758.9862.31154.45.481.3687.25243.0
    30和歌山県2805.02936.649.51141.21242.88.179.9486.47289.5
    31鳥取県2334.02628.843.51241.41371.07.080.1787.27298.1
    32島根県2832.02677.547.01224.21301.16.280.7987.64271.9
    33岡山県2816.02967.450.51170.51237.47.781.0387.67300.1
    34広島県3206.03103.559.91170.21155.67.581.0887.33254.4
    35山口県2958.02888.342.71366.21523.48.580.5186.88246.5
    36徳島県3074.02915.751.71333.21484.713.080.3286.66315.9
    37香川県2884.02928.950.61242.71266.38.380.8587.21275.7
    38愛媛県2516.02844.352.21268.81326.69.280.1686.82262.1
    39高知県2537.02686.047.41574.82257.516.580.2687.01306.0
    40福岡県2770.03052.054.41310.41406.17.980.6687.14297.0
    41佐賀県2620.02696.243.01504.21485.911.380.6587.12276.5
    42長崎県2503.02721.144.61480.81406.79.180.3886.97295.7
    43熊本県2440.02747.546.11457.11527.79.981.2287.49281.7
    44大分県2594.02768.746.41409.01279.611.381.0887.31268.5
    45宮崎県2340.02625.945.11420.91333.811.180.3487.12238.2
    46鹿児島県2407.02701.742.71535.41608.313.080.0286.78262.9
    47沖縄県2225.02696.739.21061.91082.45.780.2787.44242.6

分析コード

  • SEMestat_sem.py — クリックで表示
    # -*- coding: utf-8 -*-
    """都道府県別データ(e-Stat 社会生活統計指標 2024)による構造方程式モデリング。
    
    問い: 都道府県の健康格差は「経済的豊かさ」で説明できるのか、
          それとも「医療の供給量」なのか。そして男女で違うのか。
    
        経済的豊かさ ──┬─────────────→ 平均寿命(男)
                       │                ↗
                       └→ 入院医療キャパ ─→ 平均寿命(女)
    
    国レベル(World Bank)では豊かさと健康が同一次元に潰れたが、
    47都道府県では両者が分離するかどうかが見どころ。
    
    実行: python estat_sem.py   (先に estat_fetch.py → estat_parse.py)
    """
    import paths
    
    import numpy as np
    import pandas as pd
    import semopy
    
    from estat_parse import load_all
    
    # 列名 → (指標コード, 採用年)
    SPEC = {
        # 経済的豊かさ
        "income":     ("#C01321", 2015),   # 1人当たり県民所得
        "tax_income": ("#D02206", 2015),   # 課税対象所得(納税義務者1人当たり)
        "univ_rate":  ("#E09402", 2015),   # 高等学校卒業者の進学率
        # 入院医療キャパシティ(人口当たり)
        "nurses":     ("#I0920301", 2016),  # 看護師・准看護師数(人口10万人当たり)
        "beds":       ("#I0910203", 2015),  # 一般病院病床数(人口10万人当たり)
        "hosp":       ("#I0910103", 2015),  # 一般病院数(人口10万人当たり)
        # 健康アウトカム
        "le_m":       ("#I0520101", 2015),  # 平均余命(0歳・男)=男性の平均寿命
        "le_f":       ("#I0520102", 2015),  # 平均余命(0歳・女)=女性の平均寿命
        # 参考(モデルには入れない)
        "doctors":    ("#I0920101", 2016),  # 医療施設に従事する医師数(人口10万人当たり)
    }
    
    MODEL = """
    # ---- 測定モデル ----
    WEALTH =~ income + tax_income + univ_rate
    MED    =~ nurses + beds + hosp
    
    # ---- 構造モデル ----
    MED  ~ WEALTH
    le_m ~ WEALTH + MED
    le_f ~ WEALTH + MED
    
    # 男女の平均寿命は同じ県の同じ環境を共有する
    le_m ~~ le_f
    """
    
    USED = ["income", "tax_income", "univ_rate", "nurses", "beds", "hosp", "le_m", "le_f"]
    
    LABELS = {
        "income": "1人当たり県民所得", "tax_income": "課税対象所得", "univ_rate": "大学等進学率",
        "nurses": "看護師数", "beds": "一般病院病床数", "hosp": "一般病院数",
        "le_m": "平均寿命(男)", "le_f": "平均寿命(女)", "doctors": "医師数",
        "WEALTH": "経済的豊かさ", "MED": "入院医療キャパシティ",
    }
    
    
    def wide():
        """縦持ちの estat_long から、分析用の47行×指標 の表を作る。"""
        long = load_all()
        long = long[long.pref_cd != "00"]                 # 「全国」を落とす
        names = long.drop_duplicates("pref_cd").set_index("pref_cd")["pref"]
        w = pd.DataFrame(index=sorted(long.pref_cd.unique()))
        w.index.name = "pref_cd"
        w["pref"] = names
        for col, (code, year) in SPEC.items():
            s = long[(long.code == code) & (long.year == year)].set_index("pref_cd")["value"]
            w[col] = s
        return w
    
    
    def fit(z, desc=MODEL):
        m = semopy.Model(desc)
        m.fit(z)
        return m
    
    
    def bootstrap(z, n=600, seed=0):
        """N=47 なので漸近的な標準誤差は当てにならない。非復元でなく復元抽出で分布を見る。"""
        rng = np.random.default_rng(seed)
        keys = [("MED", "WEALTH"), ("le_m", "WEALTH"), ("le_m", "MED"),
                ("le_f", "WEALTH"), ("le_f", "MED")]
        draws = {k: [] for k in keys}
        ok = 0
        for _ in range(n):
            s = z.iloc[rng.integers(0, len(z), len(z))]
            try:
                from sem_audit import diagnose
                e, checks = diagnose(fit(s))
                if not checks["usable"]:
                    continue
            except Exception:
                continue
            e = e[e.op == "~"]
            vals = {}
            for l, r in keys:
                row = e[(e.lval == l) & (e.rval == r)]
                if row.empty:
                    break
                vals[(l, r)] = float(row["Est. Std"].iloc[0])
            if len(vals) == len(keys):
                ok += 1
                for k, v in vals.items():
                    draws[k].append(v)
        return draws, ok
    
    
    def main():
        w = wide()
        print(f"N = {len(w)} 都道府県 欠損 = {w[USED].isna().sum().sum()}\n")
    
        z = (w[USED] - w[USED].mean()) / w[USED].std()
        print("=== 観測変数の相関 ===")
        c = z.corr()
        c.index = [LABELS[i] for i in c.index]
        c.columns = [LABELS[i] for i in c.columns]
        print(c.round(2).to_string(), "\n")
    
        m = fit(z)
        s = semopy.calc_stats(m).T["Value"]
        print(f"=== 適合度 ===\nχ²/df = {s['chi2']/s['DoF']:.2f} CFI = {s['CFI']:.3f} "
              f"TLI = {s['TLI']:.3f} RMSEA = {s['RMSEA']:.3f} "
              f"(自由度 {int(s['DoF'])}、N = {len(z)})\n")
    
        e = m.inspect(std_est=True).rename(columns={"Est. Std": "std", "Std. Err": "se",
                                                    "p-value": "p"})
        lat = set(m.vars["latent"])
        load = e[(e.op == "~") & e.rval.isin(lat) & ~e.lval.isin(lat) & ~e.lval.isin(["le_m", "le_f"])]
        stru = e[(e.op == "~") & ~e.index.isin(load.index) & (e.rval != "1")]
    
        def show(t, sub):
            d = sub.copy()
            d["→"] = d.lval.map(lambda x: LABELS.get(x, x))
            d["←"] = d.rval.map(lambda x: LABELS.get(x, x))
            print(f"--- {t} ---")
            print(d[["←", "→", "std", "se", "p"]].to_string(index=False), "\n")
    
        show("因子負荷(標準化)", load)
        show("パス係数(標準化)", stru)
    
        # 残差相関で局所的な当てはまりを確認
        sigma, _ = m.calc_sigma()
        nm = list(m.vars["observed"])
        cm = lambda M: M / np.outer(np.sqrt(np.diag(M)), np.sqrt(np.diag(M)))
        R = pd.DataFrame(cm(z[nm].cov().values) - cm(sigma), index=nm, columns=nm)
        iu = np.triu_indices(len(nm), 1)
        print(f"残差相関の最大絶対値: {abs(R.values[iu]).max():.3f}\n")
    
        print("=== ブートストラップ(復元抽出 600 回、標準化係数の 95% 区間)===")
        draws, ok = bootstrap(z)
        rows = []
        for (l, r), v in draws.items():
            v = np.array(v)
            rows.append({"パス": f"{LABELS.get(r, r)} → {LABELS.get(l, l)}",
                         "中央値": np.median(v),
                         "2.5%": np.percentile(v, 2.5), "97.5%": np.percentile(v, 97.5),
                         "0をまたぐ": "はい" if np.percentile(v, 2.5) * np.percentile(v, 97.5) < 0 else "いいえ"})
        print(f"収束 {ok}/600 回")
        boot = pd.DataFrame(rows)
        print(boot.round(3).to_string(index=False), "\n")
        boot.insert(0, "key", [f"{l}<-{r}" for (l, r) in draws])
        boot.to_csv(paths.result("pref_bootstrap.csv"), index=False)
    
        # 参考: 医師数はモデルから外したが、男女差が出る変数なので生の相関だけ見ておく
        d = w[["doctors", "le_m", "le_f", "income"]].corr().round(2)
        print("=== 参考:医師数(モデル外)の相関 ===")
        print(d.loc["doctors"].to_string())
    
        w.to_csv(paths.result("pref_sample.csv"))
        e.to_csv(paths.result("pref_estimates.csv"), index=False)
        s.to_csv(paths.result("pref_fit.csv"))
        print("\n-> pref_sample.csv / pref_estimates.csv / pref_fit.csv / pref_bootstrap.csv")
    
    
    if __name__ == "__main__":
        main()
    
  • LOO診断pref_influence.py — クリックで表示
    # -*- coding: utf-8 -*-
    """都道府県2モデルの影響度診断(leave-one-out)。出力: pref_influence.csv
    
    沖縄1県で残差スクリーニングの結果が全部飛んだ経験を踏まえ、
    既存2モデルの主要パスについても「1県抜いたら係数がどれだけ動くか」を確認する。
    """
    import paths
    
    import numpy as np
    import pandas as pd
    import semopy
    
    import estat_sem as HEALTH
    import job_sem as JOB
    
    MODELS = {
        "健康": (HEALTH.MODEL, HEALTH.USED, HEALTH.wide, HEALTH.LABELS,
                 [("le_m", "WEALTH"), ("le_f", "WEALTH"), ("MED", "WEALTH"),
                  ("le_m", "MED"), ("le_f", "MED")]),
        "転職": (JOB.MODEL, JOB.USED, JOB.wide, JOB.LABELS,
                 [("jobchg", "URBAN"), ("jobchg", "PRECAR")]),
    }
    
    
    def loo(desc, used, wide, keys):
        w = wide()
        z = (w[used] - w[used].mean()) / w[used].std()
        full = fitpaths(desc, z, keys)
        rows = []
        for i, idx in enumerate(z.index):
            try:
                p = fitpaths(desc, z.drop(index=idx), keys)
            except Exception:
                continue
            for k in keys:
                rows.append({"抜いた県": w.pref.loc[idx], "path": f"{k[1]}→{k[0]}",
                             "係数": p[k], "全県": full[k], "変化": p[k] - full[k]})
        return full, pd.DataFrame(rows)
    
    
    def fitpaths(desc, z, keys):
        m = semopy.Model(desc)
        m.fit(z)
        e = m.inspect(std_est=True)
        e = e[e.op == "~"]
        out = {}
        for l, r in keys:
            row = e[(e.lval == l) & (e.rval == r)]
            out[(l, r)] = float(row["Est. Std"].iloc[0]) if len(row) else np.nan
        return out
    
    
    def main():
        allrows = []
        for name, (desc, used, wide, labels, keys) in MODELS.items():
            full, d = loo(desc, used, wide, keys)
            print(f"\n{'='*76}\n{name}モデル 1県抜きでの主要パスの振れ\n{'='*76}")
            for k in keys:
                lab = f"{labels.get(k[1], k[1])} → {labels.get(k[0], k[0])}"
                s = d[d.path == f"{k[1]}→{k[0]}"]
                worst = s.loc[s["変化"].abs().idxmax()]
                lo, hi = s["係数"].min(), s["係数"].max()
                flip = "★符号反転あり" if lo * hi < 0 else ""
                print(f"  {lab:32s} 全県 {full[k]:+.3f} 1県抜き {lo:+.3f}〜{hi:+.3f}"
                      f" 最大変化 {worst['変化']:+.3f}(除 {worst['抜いた県']}){flip}")
                allrows.append({"モデル": name, "パス": lab, "全県": full[k],
                                "LOO最小": lo, "LOO最大": hi,
                                "最大変化": worst["変化"], "その県": worst["抜いた県"]})
        pd.DataFrame(allrows).to_csv(paths.result("pref_influence.csv"), index=False)
        print("\n-> pref_influence.csv")
    
    
    if __name__ == "__main__":
        main()
    
  • 残差と図pref_residuals.py — クリックで表示
    # -*- coding: utf-8 -*-
    """「豊かさから予測される平均寿命」と実際のずれを47都道府県について出す。
    
    出力: pref_residuals.csv / pref_residual_chart.svg
    
    豊かさスコアは、SEM の因子得点ではなく3指標(県民所得・課税対象所得・大学等進学率)の
    標準化平均を使う。CFA の因子得点は課税対象所得の残差分散が 0 に潰れる Heywood ケース
    になり、実質その1指標と同じものになってしまうため。両者の結果はほぼ変わらない
    (平均寿命(男)との相関 0.482 → 0.489)。
    """
    import paths
    
    import numpy as np
    import pandas as pd
    
    from estat_sem import wide
    from make_path_diagram import BOX_LINE, FONT, INK, INK2, INK3, SURFACE, text
    
    M_COL, F_COL = "#2a78d6", "#eb6834"          # 男 / 女
    WEALTH_COLS = ["income", "tax_income", "univ_rate"]
    
    W, H = 1280, 858
    ROW = 23                                      # 1都道府県あたりの行の高さ
    DOM = (-1.85, 1.35)                           # 横軸(年)の描画範囲
    
    
    def compute():
        w = wide()
        z = (w[WEALTH_COLS] - w[WEALTH_COLS].mean()) / w[WEALTH_COLS].std()
        w["wealth"] = z.mean(axis=1)
        for y in ("le_m", "le_f"):
            b, a = np.polyfit(w.wealth, w[y], 1)
            w[y + "_hat"] = a + b * w.wealth
            w[y + "_res"] = w[y] - w[y + "_hat"]
            r = np.corrcoef(w.wealth, w[y])[0, 1]
            print(f"{y}: 傾き {b:+.3f} 年/SD r = {r:+.3f} R² = {r**2:.3f} "
                  f"残差SD = {w[y+'_res'].std():.3f} 年")
        print(f"男女の残差の相関: {np.corrcoef(w.le_m_res, w.le_f_res)[0,1]:+.3f}")
        return w.sort_values("le_m_res", ascending=False)
    
    
    def column(d, x0, y0, plot_w, start_rank):
        """1列ぶん(都道府県名・ダンベル・数値)の SVG を返す。"""
        lo, hi = DOM
        sx = lambda v: x0 + (v - lo) / (hi - lo) * plot_w
        out = []
    
        # 目盛り
        for t in (-1.5, -1.0, -0.5, 0.0, 0.5, 1.0):
            x = sx(t)
            major = t == 0
            out.append(f'<line x1="{x:.1f}" y1="{y0-16}" x2="{x:.1f}" y2="{y0+len(d)*ROW:.0f}" '
                       f'stroke="{"#b9b8b2" if major else "#eceae4"}" stroke-width="{1.4 if major else 1}"/>')
            out.append(text(x, y0 - 10, f"{t:+.1f}".replace("+0.0", "0"), 10.5, INK3, "400", "middle"))
    
        for i, (_, r) in enumerate(d.iterrows()):
            y = y0 + i * ROW + ROW / 2 + 4
            if i % 2 == 0:
                out.append(f'<rect x="{x0-92}" y="{y-15:.1f}" width="{plot_w+190}" height="{ROW}" '
                           f'fill="#faf9f6"/>')
            out.append(text(x0 - 10, y, f"{start_rank+i}", 10.5, INK3, "400", "end"))
            out.append(text(x0 - 26, y, r.pref, 12, INK2, "400", "end"))
            xm, xf = sx(r.le_m_res), sx(r.le_f_res)
            out.append(f'<line x1="{xm:.1f}" y1="{y-4:.1f}" x2="{xf:.1f}" y2="{y-4:.1f}" '
                       f'stroke="#c9c8c2" stroke-width="1.6"/>')
            for x, c in ((xf, F_COL), (xm, M_COL)):
                out.append(f'<circle cx="{x:.1f}" cy="{y-4:.1f}" r="4.6" fill="{c}" '
                           f'stroke="{SURFACE}" stroke-width="1.4"/>')
            out.append(text(x0 + plot_w + 88, y,
                            f"{r.le_m_res:+.2f} / {r.le_f_res:+.2f}", 11, INK3, "400", "end"))
        return out
    
    
    def build(d):
        r2 = {y: np.corrcoef(d.wealth, d[y])[0, 1] ** 2 for y in ("le_m", "le_f")}
        rmf = np.corrcoef(d.le_m_res, d.le_f_res)[0, 1]
        out = [f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {W} {H}" width="{W}" height="{H}">',
               f'<rect width="{W}" height="{H}" fill="{SURFACE}"/>']
        out.append(text(48, 50, "豊かさで説明できる分を差し引いた平均寿命 — 47都道府県の残差", 21, INK, "700"))
        out.append(text(48, 75, "県民所得・課税対象所得・大学等進学率の標準化平均を「豊かさ」とし、"
                                "そこから回帰で予測される平均寿命と実測値の差(年)。"
                                "平均寿命(男)の残差が大きい順。", 12.5, INK3))
    
        py = 128
        out.append(f'<rect x="40" y="{py}" width="{W-80}" height="{H-py-92}" rx="10" fill="#ffffff" '
                   f'stroke="{BOX_LINE}"/>')
        hy = py + 74
        for x0 in (200, 780):
            out.append(text(x0, py + 32, "← 予測より短命", 12, INK3))
            out.append(text(x0 + 330, py + 32, "予測より長生き →", 12, INK3, "400", "end"))
    
        half = 24
        out += column(d.iloc[:half], 200, hy, 330, 1)
        out += column(d.iloc[half:], 780, hy, 330, half + 1)
    
        ly = H - 58
        out.append(text(48, ly, "凡例", 12, INK, "700"))
        for x, c, lab in ((100, M_COL, "平均寿命(男)の残差"), (300, F_COL, "平均寿命(女)の残差")):
            out.append(f'<circle cx="{x}" cy="{ly-4}" r="4.6" fill="{c}"/>')
            out.append(text(x + 12, ly, lab, 12, INK2))
        out.append(text(500, ly, "数値は 男 / 女(年)", 12, INK3))
        out.append(text(48, ly + 20,
                        f"※ 豊かさで説明できるのは男性で {r2['le_m']*100:.0f}%(R²={r2['le_m']:.3f})、"
                        f"女性では {r2['le_f']*100:.0f}%(R²={r2['le_f']:.3f})。"
                        "女性の残差は実質そのまま平均寿命の順位であり、豊かさを差し引いた効果ではない。", 11, INK3))
        out.append(text(48, ly + 38,
                        "※ 東京都は豊かさスコアが突出した高レバレッジ点で、予測値が外挿になっている。"
                        f"男女の残差の相関は {rmf:.2f} で、豊かさ以外の要因は男女で共通している。", 11, INK3))
        out.append(f'<text x="{W-48}" y="{ly+38}" text-anchor="end" font-family="{FONT}" '
                   f'font-size="11" fill="{INK3}">出典: 総務省統計局 e-Stat 社会・人口統計体系</text>')
        out.append("</svg>")
        return "\n".join(out)
    
    
    if __name__ == "__main__":
        d = compute()
        keep = ["pref", "wealth", "income", "tax_income", "univ_rate",
                "le_m", "le_m_hat", "le_m_res", "le_f", "le_f_hat", "le_f_res"]
        d[keep].to_csv(paths.result("pref_residuals.csv"), index=False)
        svg = build(d)
        with open(paths.figure("pref_residual_chart.svg"), "w") as f:
            f.write(svg)
        print(f"\n-> pref_residuals.csv / pref_residual_chart.svg ({len(svg)/1024:.1f} KB)")
    
  • 図の生成make_pref_diagram.py — クリックで表示
    # -*- coding: utf-8 -*-
    """都道府県SEMのパス図を SVG で生成する。出力: pref_path_diagram.svg"""
    import paths
    
    import math
    
    import numpy as np
    import pandas as pd
    import semopy
    
    from estat_sem import LABELS, MODEL, USED, wide
    from make_path_diagram import (BOX_LINE, C_NEG, C_NS, C_POS, FONT, INK, INK2, INK3,
                                   SURFACE, Node, arrow, esc, path_style, stars, loading_label, text)
    
    W, H = 1280, 932
    P = 104                      # パネル上端
    PH = 654                     # パネル高さ
    
    
    def covariance(a, b, label, bulge=105):
        """両端矢印の曲線(相関を表す)。a, b の右側にふくらませる。"""
        x1, y1 = a.cx + a.w / 2, a.cy
        x2, y2 = b.cx + b.w / 2, b.cy
        cx, cy = max(x1, x2) + bulge, (y1 + y2) / 2
        d = f"M{x1},{y1} Q{cx},{cy} {x2},{y2}"
        lx, ly = .25 * x1 + .5 * cx + .25 * x2, (y1 + y2) / 2
        s = (f'<path d="{d}" fill="none" stroke="{INK3}" stroke-width="1.5" '
             f'marker-end="url(#ah-{INK3[1:]})" marker-start="url(#ah-{INK3[1:]})"/>')
        bw = len(label) * 7.4 + 10
        s += (f'<rect x="{lx-bw/2:.1f}" y="{ly-10}" width="{bw:.1f}" height="20" rx="4" '
              f'fill="{SURFACE}" opacity="0.95"/>' + text(lx, ly + 4, label, 12, INK3, "600", "middle"))
        return s
    
    
    def build():
        w = wide()
        z = (w[USED] - w[USED].mean()) / w[USED].std()
        m = semopy.Model(MODEL)
        m.fit(z)
        e = m.inspect(std_est=True)
        s = semopy.calc_stats(m).T["Value"]
        est = {(r.lval, r.rval, r.op): (float(r["Est. Std"]),
                                        None if r["p-value"] == "-" else float(r["p-value"]))
               for _, r in e.iterrows()}
        boot = pd.read_csv(paths.result("pref_bootstrap.csv")).set_index("key")
    
        out = [f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {W} {H}" width="{W}" height="{H}">',
               f'<rect width="{W}" height="{H}" fill="{SURFACE}"/>', "<defs>"]
        for c in (C_POS, C_NEG, C_NS, INK3):
            out.append(f'<marker id="ah-{c[1:]}" viewBox="0 0 10 10" refX="9" refY="5" '
                       f'markerWidth="7" markerHeight="7" orient="auto-start-reverse">'
                       f'<path d="M0,1 L9,5 L0,9 z" fill="{c}"/></marker>')
        out.append("</defs>")
    
        out.append(text(48, 52, "都道府県の健康格差は、豊かさか、医療の量か — 47都道府県の構造方程式モデリング(SEM)",
                        22, INK, "700"))
        out.append(text(48, 78, "e-Stat 社会・人口統計体系「社会生活統計指標-都道府県の指標-2024」"
                                "(2015年前後の値) semopy 2.3.11・最尤法・全変数を標準化", 13, INK3))
    
        out.append(f'<rect x="40" y="{P}" width="{W-80}" height="{PH}" rx="10" fill="#ffffff" '
                   f'stroke="{BOX_LINE}"/>')
        out.append(text(64, P + 32, "経済的豊かさ・入院医療キャパシティ → 男女別の平均寿命", 16, INK, "700"))
        out.append(text(64, P + 56,
                        f"N = 47 都道府県 χ²/df = {s['chi2']/s['DoF']:.2f} CFI = {s['CFI']:.3f} "
                        f"TLI = {s['TLI']:.3f} RMSEA = {s['RMSEA']:.3f} 残差相関の最大 |r| = 0.142",
                        12.5, INK3))
    
        WEALTH = Node(400, P + 180, 172, 66, "経済的豊かさ", True)
        MED = Node(400, P + 430, 210, 66, "入院医療キャパシティ", True, "人口当たり")
        LE_M = Node(985, P + 185, 224, 58, "平均寿命(男)", False, "78.7〜81.8年(青森〜滋賀)")
        LE_F = Node(985, P + 430, 224, 58, "平均寿命(女)", False, "85.9〜87.7年(青森〜長野)")
    
        wb = [Node(150, P + 125 + i * 55, 196, 38, LABELS[k])
              for i, k in enumerate(["income", "tax_income", "univ_rate"])]
        mb = [Node(150, P + 375 + i * 55, 196, 38, LABELS[k])
              for i, k in enumerate(["nurses", "beds", "hosp"])]
    
        for lat, boxes, keys in [(WEALTH, wb, ["income", "tax_income", "univ_rate"]),
                                 (MED, mb, ["nurses", "beds", "hosp"])]:
            for n, k in zip(boxes, keys):
                out.append(arrow(lat, n, loading_label(*est[(k, lat_key(lat), '~')]), INK3, 1.3, lab_t=.46))
    
        for src, dst, key, t in [(WEALTH, MED, ("MED", "WEALTH"), .5),
                                 (WEALTH, LE_M, ("le_m", "WEALTH"), .5),
                                 (WEALTH, LE_F, ("le_f", "WEALTH"), .30),
                                 (MED, LE_M, ("le_m", "MED"), .30),
                                 (MED, LE_F, ("le_f", "MED"), .5)]:
            v, p = est[(key[0], key[1], "~")]
            c, wd, d = path_style(v, p)
            out.append(arrow(src, dst, f"{v:.2f}{stars(p)}", c, wd, d, lab_t=t))
    
        cov = est.get(("le_m", "le_f", "~~")) or est.get(("le_f", "le_m", "~~"))
        out.append(covariance(LE_M, LE_F, f"残差相関 {cov[0]:.2f}"))
    
        for n in (WEALTH, MED, LE_M, LE_F, *wb, *mb):
            out.append(n.svg())
    
        # ---- 国レベルとの対比 ----
        by = P + 522
        out.append(f'<rect x="60" y="{by}" width="520" height="112" rx="8" fill="#f2f7fd" '
                   f'stroke="#cfe0f5"/>')
        out.append(text(78, by + 24, "国レベル(World Bank 128か国)との対比", 12.5, "#1c5cab", "700"))
        for i, line in enumerate([
                "国の比較では「人的資本」と「経済的繁栄」が標準化係数 0.97 で",
                "潰れ、別々の潜在変数として立てられなかった。47都道府県では",
                "所得と平均寿命(女)の相関が 0.06 しかなく、豊かさと健康は",
                "はっきり別の次元として分離する。"]):
            out.append(text(78, by + 46 + i * 17, line, 11.5, INK2))
    
        # ---- ブートストラップ 95% 区間 ----
        out.append(f'<rect x="600" y="{by}" width="630" height="112" rx="8" fill="#fdf8ef" '
                   f'stroke="#e8d9b8"/>')
        out.append(text(618, by + 24, "ブートストラップ(復元抽出600回)標準化係数の 95% 区間",
                        12.5, "#8a6d1f", "700"))
        rows = [("MED<-WEALTH", "豊かさ → 医療キャパ"), ("le_m<-WEALTH", "豊かさ → 平均寿命(男)"),
                ("le_m<-MED", "医療キャパ → 平均寿命(男)"), ("le_f<-WEALTH", "豊かさ → 平均寿命(女)"),
                ("le_f<-MED", "医療キャパ → 平均寿命(女)")]
        for i, (k, lab) in enumerate(rows):
            r = boot.loc[k]
            y = by + 46 + i * 13.5
            zero = r["0をまたぐ"] == "はい"
            col = INK3 if zero else INK2
            out.append(text(618, y, lab, 11, col))
            out.append(text(830, y, f"{r['中央値']:+.2f}", 11, col, "600", "end"))
            out.append(text(960, y, f"[{r['2.5%']:+.2f}, {r['97.5%']:+.2f}]", 11, col, "400", "end"))
            out.append(text(980, y, "0をまたぐ(効果を示せない)" if zero else "0を含まない", 11, col))
    
        # ---- 凡例 ----
        lg = H - 108
        out.append(text(48, lg, "凡例", 12, INK, "700"))
        out.append(f'<ellipse cx="110" cy="{lg-5}" rx="21" ry="11" fill="#eef4fd" '
                   f'stroke="{C_POS}" stroke-width="2"/>')
        out.append(text(140, lg, "潜在変数", 12, INK2))
        out.append(f'<rect x="206" y="{lg-16}" width="42" height="22" rx="4" fill="#ffffff" '
                   f'stroke="{BOX_LINE}" stroke-width="1.5"/>')
        out.append(text(256, lg, "観測変数", 12, INK2))
        for x, col, wd, ds, lab in [(330, C_POS, 2.4, "", "正の効果(有意)"),
                                    (500, C_NEG, 2.4, "", "負の効果(有意)"),
                                    (670, C_NS, 1.6, ' stroke-dasharray="6 4"', "非有意")]:
            out.append(f'<path d="M{x},{lg-5} L{x+52},{lg-5}" stroke="{col}" stroke-width="{wd}"'
                       f'{ds} marker-end="url(#ah-{col[1:]})"/>')
            out.append(text(x + 60, lg, lab, 12, INK2))
        out.append(text(790, lg, "数値は標準化係数 *** p<.001 ** p<.01 * p<.05", 12, INK3))
        out.append(text(48, lg + 22,
                        "※ N = 47 に対して推定パラメータが多く、RMSEA は小標本で過大に出る。"
                        "有意性の判断はブートストラップ区間を優先すること。", 11, INK3))
        out.append(text(48, lg + 40,
                        "※ 医師数は他指標との残差相関が大きい(医学部の所在に規定される)ため、"
                        "入院医療キャパシティの指標から除外した。", 11, INK3))
        inf = pd.read_csv(paths.result("pref_influence.csv"))
        inf = inf[inf["モデル"] == "健康"]
        g = lambda k: inf[inf["パス"].str.contains(k)].iloc[0]
        a, b = g("平均寿命\\(男\\)$"), g("入院医療キャパシティ$")
        out.append(text(48, lg + 58,
                        f"※ 1県抜き(47通り)での振れ: 豊かさ→平均寿命(男) {a['LOO最小']:+.2f}〜{a['LOO最大']:+.2f}、"
                        f"豊かさ→医療キャパ {b['LOO最小']:+.2f}〜{b['LOO最大']:+.2f}。"
                        "区間が0を含まない2本はどちらも符号が反転しない。", 11, INK3))
        out.append(f'<text x="{W-48}" y="{lg+22}" text-anchor="end" font-family="{FONT}" '
                   f'font-size="11" fill="{INK3}">出典: 総務省統計局 e-Stat 社会・人口統計体系</text>')
    
        out.append(text(48, H - 25,
                        "因子負荷量:細い灰色の矢印。† 非標準化係数を1に固定(検定対象外)。n.s. は p≥.05。", 11, INK3))
        out.append(text(48, H - 9,
                        "星印は非標準化係数の漸近的な検定結果。表示する数値は標準化係数。", 11, INK3))
        out.append("</svg>")
        return "\n".join(out)
    
    
    def lat_key(node):
        return "WEALTH" if node.label == "経済的豊かさ" else "MED"
    
    
    if __name__ == "__main__":
        svg = build()
        with open(paths.figure("pref_path_diagram.svg"), "w") as f:
            f.write(svg)
        print(f"-> pref_path_diagram.svg  ({len(svg)/1024:.1f} KB)")
    

この分析の詳細ドキュメント →

分析を再現する

Python 3.13 が必要(3.14 には semopy の wheel が無い)。 make setup && make all で全分析と全図が約50秒で再生成される。 data/raw/ に生データをコミットしてあるためネットワークは不要。