Up

3点識別試験

弁別確率のベイズ推定

 

3つの刺激の内、2つが同じ刺激Aで1つが異なる刺激Bであるとき、3つの中で他の2つと異なるものとして正しくBが選ばれる頻度を調べる実験(3点識別試験:triangular test)を想定する。なお、刺激Aに対して、2種類の刺激特性BCを考え、BCの組み合わせに対する交互作用をロジスティック回帰モデル(Gelmanら、20143.7節;岡本、2014、第4章)により分析するモデルを、本ウェブサイトの後半で扱う。

本ウェブサイトのプログラムファイルは圧縮ファイルTriTestPdFiles.zipとして纏めたので、自由にダウンロードして利用されたい。

 

いま、正しくBが異なると判る確率をと置く。このPdproportion of distinguishersを表すとされているが、信号検出理論(signal detection theory)に倣えばprobability of detection/discriminationを表すと考えた方が使い易いと思われる。同じ観察者に、配置を変えて複数回提示する場合は、確率と考えればよい。確率PdBが異なることが検出できるとき、確率でどれが異なるか判らないということになる。このとき、3つの刺激からランダムに1つが他の2つと異なるものとして選ばれるが、このランダムに選ばれた刺激がBである確率はである。したがって、正しくBが異なるものとして検出される場合と、ランダムにBが選ばれる場合とを合わせると、Bが選ばれる確率は

となる。確率は見かけの正答確率Pcprobability of correct response)である。

総試行数の実験において、Bが選ばれた試行数をkとおくと、kは試行数Nの2項分布における成功試行数と考えて、確率は

で与えられる。

確率モデル(2)に基づいてPdの事後分布を求めるStanスクリプトを本ウェブサイトの後半に示すリスト1のように用意した。

リスト1のStanスクリプトを実行するPythonスクリプトをリスト2のように用意した。

リスト2のスクリプトを実行すると、総試行数Nと正答試行数kの入力が求められる。

 

(py39) PS D:\XXXXX\TriTestPdFiles> python .\TriTest.py

INFO:numexpr.utils:NumExpr defaulting to 8 threads.

N = 50

k = 25

 

上の例では、Nの値として50、kの値として25が設定されている。

kの値を設定すると、計算が始まる。

計算が終了すると、Pdの事後分布が図1のように表示される。

図1

 

図1では、弁別確率Pdのモードが0.27であること、95HDI[0.06, 0.44]であることが、キャプションに表示されている。

図1のグラフは、ファイルFigPd.pngとして保存されている。

 

 

ロジスティック回帰モデル

 

まず、刺激Aに対して、2つの刺激特性BCについて、Bのみの刺激BCのみの刺激C、およびBCの2つの特性をもつ刺激BCについて、それぞれ3点識別試験を行った場合を考える。特性BあるいはCが単独の場合に比べて、BCが同時の場合、その効果を調べるためにロジスティック回帰モデルを用いる。

続いて、識別確率が年齢とともにどう変わるかをロジスティック回帰モデルで調べる場合を、この後半で扱う。

 

ロジスティック回帰モデルでは、確率pのロジット変換値が線形モデルで表されるとする(Gelmanら、20143.7節;岡本、2014、第4章)。

ロジット変換は次式で表される。

これより、次式を得る。

ロジスティック回帰モデルでは、が回帰モデルで表される。

刺激BCBCの弁別確率PdBPdCPdBCを、刺激Bの主効果fB、刺激Cの主効果fC、交互採用fBCを設定して次式のように表す。

上式のモデルのStanスクリプトをリスト3に示した。スクリプトファイルは、圧縮ファイルTriTestPdFiles.zipにまとめた。ダウンロード・展開すれば、自由に利用できる。

リスト3のStanスクリプトを用いるPythonスクリプトはリスト4に示した。

リスト4のスクリプトを実行すると、まず刺激B、刺激C、刺激BCに対する正答試行数kおよび総試行数の入力が求められる。次の例では、刺激A、A’、Bの3点の組み合わせの場合、成功試行数kB25試行、総試行数NB50試行のデータであることを示している。刺激CおよびBCに対しても同様である。

 

(py39) PS D:\xxxxx\TriTestPdFiles> python .\TriTestLogistic.py

INFO:numexpr.utils:NumExpr defaulting to 8 threads.

kB = 25

NB = 50

kC = 17

NC = 50

kBC = 40

NBC = 50

 

最後のNBC = 50を入力すると、計算が始まる。

計算が終了すると、効果fBfCfBCの事後分布のグラフが表示される(図2)。

図2

 

主効果fBfCは、負の値の方向に絶対値が大きいほど効果がない(Pdが0に近付く)ことを表すが、交互作用fBCは0であれば交互作用は無い、0からはなれた値であれば交互作用ありとなる。図5ではfBC95HDIは[2.75, 10.09]と原点0から右方向に分布が明瞭にシフトしている。

図2のフォームを閉じると、次に弁別確率Pdの事後分布の統計量の計算が始まり、終了すると図3のグラフが表示される。

図3

 

刺激BPdはモードが0.23であるが、刺激CPdはモードが0.01と0に近い。すなわち、刺激BAと確率0.23で弁別できているが、刺激Cは弁別できていない。刺激BCの混合では、弁別確率PdBCはモードが0.70でよく弁別されていると言える。刺激Cは隠し味みたいなもので、刺激Bと組み合わされるとその効果が明瞭になると言えるかもしれない。

図2と図3のグラフは、自動的にファイル名Figfctrs.pngFigPd.pngに保存される。

 

データの例として、上とは異なる次の場合を分析してみた。

 

(py39) PS D:\xxxxd\TriTestPdFiles> python .\TriTestLogistic.py

INFO:numexpr.utils:NumExpr defaulting to 8 threads.

kB = 30

NB = 50

kC = 30

NC = 50

kBC = 20

NBC = 50

 

各効果のグラフは図4のようになった。

図4

 

交互作用fBCの事後分布が0から負の方向にかなり寄っている。

図5

 

刺激Bと刺激Cの弁別確率PdBPdCはモードが0.38でかなり弁別されていると言えるが、刺激BCの場合は、刺激Bおよび刺激Cの複合であるのに関わらずPdBCのモードは0.00であり、弁別されていない。2つの刺激BCの混合刺激は弁別が難しくなり、これは交互作用fBCの分布が負の領域に偏っていることに表されている(図4)。

 

識別確率の年齢による変化(仮想データ)

 

上で説明したように、ロジット変換は次式で表される。

これより、次式を得る。

ロジスティック回帰モデルでは、が回帰モデルで表される。いまの場合、独立変数である年齢をAで表せば、

である。このとき、識別確率Pdは次式で与えられる。

したがって、正答確率Pc

となる。

上のモデルをStanスクリプトで表したものをリストRA1に示す。

リストRA1Stanスクリプトを利用するPythonスクリプトをリストRA2に示す。

リストRA2のプログラム(TriLogRegAge.py)では、入力データは図RA1に示す様式のCSVファイルとして用意する。

RA1

 

1行目に変数名を書く。1列目がでーたの識別変数で数値でも文字列でもよい。2列目に判断の正誤をかく。正答であれば1,誤答であれば0である。3列目に回帰モデルの独立変数の値を書く。図RA1のデータでは、データの識別変数の値が1である先頭のデータは、判断が正答(1で表されている)で、独立変数(図RA1の場合の場合、年齢)が33歳である。

RA1のように作成されたデータは、保存のときにファイル拡張子として「.csv」を選ぶと、CSV形式で保存される。

リストRA2のスクリプト(TriLogRegAge.py)を実行すると入力ファイル名の設定が求められる。

 

(py39) PS D:\xxxxx\TriTestPdFiles> python TriLogRegAge.py

INFO:numexpr.utils:NumExpr defaulting to 8 threads.

Input file (*.csv) = Data.csv

 

上の例では、入力ファイル名Data.csvが設定されいる。

入力ファイル名を設定して、Enterキーを押すと計算が始まる。MCMCサンプリングが終了すると、まずパラメータabの事後分布のグラフが表示される(図RA2)。

RA

 

RA2の表示されているフォームを閉じると、図RA3のフォームが表示される。

RA

 

RA3より、独立変数Aの係数bの事後分布は負の領域にあり、mode-1.09(この値は、MCMCサンプリングに基づくものであるので、プログラムの実行ごとに多少変動がある)で、独立変数Aの値が大きくなる程、識別確率Pdと正答確率Pcは小さくなると考えられる。

RA3のフォームを閉じると、図RA4のフォームが表示される。

RA

 

識別確率Pd(赤曲線)と正答確率Pc(青曲線)のグラフは、事後分布のmodeをパラメータ値とするグラフである。

緑の小円は、年齢の範囲を4等分した区間に分け、各区間内の年齢の正答比率である。正答確率Pcのグラフとデータの小円はよく合っていると言える。

 

 

なお、Python用のStanであるPyStanの説明を<岡本安晴「いまさら聞けないPythonでデータ分析」丸善出版>で行っている。

 

引用文献

Gelman, A., Carlin, J. B., Stern, H. S., Dunson, D. B., Vehtari, A., & Rubin, D. B. (2014). Bayesian data analysis, third edition. CRC Press.

岡本安晴(2014)心理学データ分析と測定.勁草書房

 

 

リスト1 3点試験法のStanスクリプト(ファイル名tri_test.stan

data {

    int N;

    int k;

}

parameters {

    real<lower = 0.0, upper = 1.0> Pd;  //  Discrimination Probability

}

transformed parameters {

    real Pc;                            //  Correct Response Probability

     Pc = Pd + (1.0 - Pd) * (1/3.0);

}

model {

    Pd ~ uniform(0.0, 1.0);

    k ~ binomial(N, Pc);

}

 

 

リスト2 リスト1のStanスクリプトを利用するPythonスクリプト(ファイル名TriTest.py

import numpy as np

import scipy.stats as ss

import matplotlib.pyplot as plt

import pystan

import seaborn as sb

import pickle

 

 

def CalcMAPEst(samples, a = 0.05, n_points = 10000):

    """

        Calculatte a MAP estimate from a KDE graph on [Lp, Up]

        Lp and Up are 100*a/2 and 100(1-a/2) percentile points of samples

    """

    print('Calculating the mode.')

    Lp, Up = np.percentile(samples, [100 * a/2, 100 * (1 - a/2)])  # import numpy as np

    coord = np.linspace(Lp, Up, n_points)

    est_pdf = ss.gaussian_kde(samples).pdf(coord)  # import scipy.stats as ss

    map_idx = np.argmax(est_pdf)

    MAP_Est = coord[map_idx]      

    return MAP_Est, est_pdf[map_idx]

 

 

def calcHDI(sample, a = 0.05):

    """

            Calculation of 100(1-a)% HDI for sample

            Chen-Shao HPD Estimation algorithm

    """

    print('Calculating HDI.')

    smpl = np.sort(sample)

    n = len(smpl)

    ckW = max(smpl) - min(smpl)

    cki = 0

    RB = int(n * (1-a))

    L0 = 0

    U0 = n-1

    while (RB + cki < n):

        if smpl[RB + cki] - smpl[cki] < ckW:

            ckW = smpl[RB + cki] - smpl[cki]

            L0 = cki

            U0 = RB + cki

        cki += 1

    ckW = max(smpl) - min(smpl)

    cki = 1

    LB = n - int(n * (1-a))

    L1 = 0

    U1 = n-1

    while (LB - cki >= 0):

        if smpl[n - cki] - smpl[LB - cki] < ckW:

            ckW = smpl[n - cki] - smpl[LB - cki]

            U1 = n - cki

            L1 = LB - cki

        cki += 1

    return smpl[int((L1+L0)/2)], smpl[int((U1+U0)/2)]

 

 

 

N = int(input('N = '))

k = int(input('k = '))

 

Data = {'N': N, 'k': k}

 

sm = pystan.StanModel(file = 'tri_test.stan')

   

fit = sm.sampling(data = Data, iter = 10_000, n_jobs = 1)

 

print(fit)

 

sb.kdeplot(fit['Pd'])

plt.hist(fit['Pd'], density = True, color = 'g', alpha = 0.5)

 

MAPEst = CalcMAPEst(fit['Pd'])[0]

plt.scatter([MAPEst], [0], marker = 'o', s = 200,

            c = 'magenta', label = f'mode = {MAPEst:.2f}')

L_bndry, R_bndry = calcHDI(fit['Pd'], 0.05)

plt.plot([L_bndry, R_bndry], [0, 0], c = 'r', lw = 5,

         label = '95% HDI [{0:.2f}, {1:.2f}]'.format(L_bndry, R_bndry))

plt.yticks([])

plt.xlabel('Pd', fontsize = 12)

plt.title(f'Posterior distribution\nN = {N}, k = {k}', fontsize = 16)

plt.legend()

plt.savefig('FigPd.png')

plt.show()

 

 

リスト3 ロジスティック回帰モデルのStanスクリプト(ファイル名tri_test_logistic.stan

//

//      Yasuharu Okamoto,  2021.12

//

data {

    int NB;

    int kB;

    int NC;

    int kC;

    int NBC;

    int kBC;   

}

parameters {

    real fB;

    real fC;

    real fBC;

}

transformed parameters {

    real<lower = 0.0, upper = 1.0> PdB;

    real<lower = 0.0, upper = 1.0> PdC;

    real<lower = 0.0, upper = 1.0> PdBC;

    real<lower = 0.0, upper = 1.0> PcB;

    real<lower = 0.0, upper = 1.0> PcC;

    real<lower = 0.0, upper = 1.0> PcBC;

   

    PdB = 1.0 / (1.0 + exp(-fB));

    PdC = 1.0 / (1.0 + exp(-fC));

    PdBC = 1.0 / (1.0 + exp(-(fB + fC + fBC)));

   

    PcB = PdB + (1.0 - PdB) * (1.0 / 3.0);

    PcC = PdC + (1.0 - PdC) * (1.0 / 3.0);

    PcBC = PdBC + (1.0 - PdBC) * (1.0 / 3.0);

}

model {

    fB ~ normal(0.0, 5.0);

    fC ~ normal(0.0, 5.0);

    fBC ~ normal(0.0, 5.0);

    kB ~ binomial(NB, PcB);

    kC ~ binomial(NC, PcC);

    kBC ~ binomial(NBC, PcBC);

}

 

 

リスト4 リスト3のStanスクリプトをPythonスクリプト(ファイル名TriTestLogistic.py

import numpy as np

import scipy.stats as ss

import pystan

import matplotlib.pyplot as plt

import seaborn as sb

import pickle

 

 

def CalcMAPEst(samples, a = 0.05, n_points = 10000):

    """

        Calculatte a MAP estimate from a KDE graph on [Lp, Up]

        Lp and Up are 100*a/2 and 100(1-a/2) percentile points of samples

    """

    print('Calculating the mode.')

    Lp, Up = np.percentile(samples, [100 * a/2, 100 * (1 - a/2)])  # import numpy as np

    coord = np.linspace(Lp, Up, n_points)

    est_pdf = ss.gaussian_kde(samples).pdf(coord)  # import scipy.stats as ss

    map_idx = np.argmax(est_pdf)

    MAP_Est = coord[map_idx]      

    return MAP_Est, est_pdf[map_idx]

 

 

def calcHDI(sample, a = 0.05):

    """

            Calculation of 100(1-a)% HDI for sample

            Chen-Shao HPD Estimation algorithm

    """

    print('Calculating the HDI.')

    smpl = np.sort(sample)

    n = len(smpl)

    ckW = max(smpl) - min(smpl)

    cki = 0

    RB = int(n * (1-a))

    L0 = 0

    U0 = n-1

    while (RB + cki < n):

        if smpl[RB + cki] - smpl[cki] < ckW:

            ckW = smpl[RB + cki] - smpl[cki]

            L0 = cki

            U0 = RB + cki

        cki += 1

    ckW = max(smpl) - min(smpl)

    cki = 1

    LB = n - int(n * (1-a))

    L1 = 0

    U1 = n-1

    while (LB - cki >= 0):

        if smpl[n - cki] - smpl[LB - cki] < ckW:

            ckW = smpl[n - cki] - smpl[LB - cki]

            U1 = n - cki

            L1 = LB - cki

        cki += 1

    return smpl[int((L1+L0)/2)], smpl[int((U1+U0)/2)]

 

 

kB = int(input('kB = '))

NB = int(input('NB = '))

kC = int(input('kC = '))

NC = int(input('NC = '))

kBC = int(input('kBC = '))

NBC = int(input('NBC = '))

 

 

sm = pystan.StanModel(file = 'tri_test_logistic.stan')

 

Data = {'kB': kB, 'NB': NB, 'kC': kC, 'NC': NC, 'kBC': kBC, 'NBC': NBC}

fit = sm.sampling(data = Data, iter = 10_000, n_jobs = 1)

print(fit)

 

MAPfB = CalcMAPEst(fit['fB'])[0]

MAPfC = CalcMAPEst(fit['fC'])[0]

MAPfBC = CalcMAPEst(fit['fBC'])[0]

L_HDIfB, R_HDIfB = calcHDI(fit['fB'])

L_HDIfC, R_HDIfC = calcHDI(fit['fC'])

L_HDIfBC, R_HDIfBC = calcHDI(fit['fBC'])

sb.kdeplot(fit['fB'], label = 'fB, mode = {0:.2f}, \n    95% HDI = [{1:.2f}, {2:.2f}]'.

           format(MAPfB, L_HDIfB, R_HDIfB))

sb.kdeplot(fit['fC'], label = 'fC, mode = {0:.2f}, \n    95% HDI = [{1:.2f}, {2:.2f}]'.

           format(MAPfC, L_HDIfC, R_HDIfC))

sb.kdeplot(fit['fBC'], label = 'fBC, mode = {0:.2f}, \n     95% HDI = [{1:.2f}, {2:.2f}]'.

           format(MAPfBC, L_HDIfBC, R_HDIfBC))

plt.xlabel('Factor', fontsize = 14)

plt.yticks([])

plt.title('Posterior distribtion\n' + \

          ('kB = {0:}, NB = {1:}, kC = {2:}, NC = {3:}\n'.format(kB, NB, kC, NC)) +\

          ('kBC = {0:}, NBC = {1:}'.format(kBC, NBC)), fontsize = 16)

plt.legend()

plt.tight_layout()

plt.savefig('FigFctrs.png')

plt.show()

 

MAPPB = CalcMAPEst(fit['PdB'])[0]

MAPPC = CalcMAPEst(fit['PdC'])[0]

MAPPBC = CalcMAPEst(fit['PdBC'])[0]

L_HDIPB, R_HDIPB = calcHDI(fit['PdB'])

L_HDIPC, R_HDIPC = calcHDI(fit['PdC'])

L_HDIPBC, R_HDIPBC = calcHDI(fit['PdBC'])

sb.kdeplot(fit['PdB'], label = 'PdB, mode = {0:.2f}, \n    95% HDI = [{1:.2f}, {2:.2f}]'.

           format(MAPPB, L_HDIPB, R_HDIPB))

sb.kdeplot(fit['PdC'], label = 'PdC, mode = {0:.2f}, \n    95% HDI = [{1:.2f}, {2:.2f}]'.

           format(MAPPC, L_HDIPC, R_HDIPC))

sb.kdeplot(fit['PdBC'], label = 'PdBC, mode = {0:.2f}, \n     95% HDI = [{1:.2f}, {2:.2f}]'.

           format(MAPPBC, L_HDIPBC, R_HDIPBC))

plt.xlabel('Prob. of detection', fontsize = 14)

plt.yticks([])

plt.title('Posterior distribtion\n' + \

          ('kB = {0:}, NB = {1:}, kC = {2:}, NC = {3:}\n'.format(kB, NB, kC, NC)) +\

          ('kBC = {0:}, NBC = {1:}'.format(kBC, NBC)), fontsize = 16)

plt.legend()

plt.tight_layout()

plt.savefig('FigPd.png')

plt.show()

 

 

リストRA1. 年齢Aを独立変数(スクリプトではAs)とするロジスティック回帰モデルのStanスクリプト(ファイル名reg_age.stan

data {

    int N;

    int<lower = 0, upper = 1> Rs[N];

    real As[N];

}

parameters {

    real a;

    real b;

}

transformed parameters {

    real <lower = 0.0, upper = 1.0> Pd[N];

    real<lower = 0.0, upper = 1.0> Pc[N];  //  Probability of correct response

    for (i in 1:N) {

        Pd[i] = inv_logit(a + b * As[i]);  //  Logistic regression model

        Pc[i] = Pd[i] + (1.0 - Pd[i]) * (1.0/3.0);

    }

}

model {

    a ~ normal(0.0, 10.0);    //   weakly informative priot

    b ~ normal(0.0, 10.0);    //   weakly informative priot

    Rs ~ bernoulli(Pc);

}

 

 

リストRA2.リストRA1Stanスクリプトを用いるPythonスクリプト(ファイル名TriLogRegAge.py

import csv

from copy import deepcopy

import numpy as np

import scipy.stats as ss

import pystan

import matplotlib.pyplot as plt

import seaborn as sb

 

#       日本語表記の設定

plt.rcParams['font.family'] = ['Yu Gothic', 'Noto Sans CJK JP', 'Hiragino Maru Gothic Pro']

 

fin_nm = input('Input file (*.csv) = ')

with open(fin_nm, 'r') as f:

    Data_in = [v for v in csv.reader(f)]

 

Data_in = np.array(Data_in)

Rs = [int(v) for v in Data_in.T[1][1:]]

As = [float(v) for v in Data_in.T[2][1:]]

 

print(len(Rs), len(As))

N = len(Rs)

Rs = np.array(Rs)

As = np.array(As)

Raw_As = deepcopy(As)

As_mean = As.mean()

As_sd = As.std()

As = (As - As_mean) / As_sd

 

#       Stanスクリプトのコンパイル

sm = pystan.StanModel(file = 'reg_age.stan')

 

#        MCMCサンプリング

fit = sm.sampling(data = {'N':N, 'Rs': Rs, 'As': As},

                  pars = ['a', 'b'], iter = 10_000, n_jobs = 1)

print(fit)

 

#

#      modeの推定

#

def est_mode(samples, a = 0.05, n_points = 10_000):

    import numpy as np

    import scipy.stats as ss

    Lp, Up = np.percentile(samples, [100 * a/2, 100 * (1 - a/2)])  # import numpy as np

    coord = np.linspace(Lp, Up, n_points)

    est_pdf = ss.gaussian_kde(samples).pdf(coord)  # import scipy.stats as ss

    map_idx = np.argmax(est_pdf)

    MAP_Est = coord[map_idx]  

    return MAP_Est

 

a_mode = est_mode(fit['a'])

b_mode = est_mode(fit['b'])

 

#     パラメータaの事後分布のグラフ

sb.kdeplot(fit['a'], label = f'mode = {a_mode:.2f}')

plt.xlabel('a', fontsize = 14)

plt.yticks([])

plt.title('aの事後分布', fontsize = 16)

plt.legend(fontsize = 14)

plt.savefig('Fig_a.png')

plt.show()

#     パラメータbの事後分布のグラフ

sb.kdeplot(fit['b'], label = f'mode = {b_mode:.2f}')

plt.xlabel('b', fontsize = 14)

plt.yticks([])

plt.title('bの事後分布', fontsize = 16)

plt.legend(fontsize = 14)

plt.savefig('Fig_b.png')

plt.show()

#

#        値のカテゴリ化

#    0:[-inf, 29), 1:[30, 39), 2:[40, 49), 3:[50, +inf)

#

def check_cat(v):

    v = int((v - 20) / 10)

    if v > 3:

        v = 3

    return v

 

Ones = np.zeros(4)    #   各カテゴリにおける判断1の数

Boths = np.zeros(4)   #   各カテゴリにおける判断1および0の数

for r, a in zip(Rs, Raw_As):

    icat = check_cat(a)

    Boths[icat] += 1

    if r == 1:

        Ones[icat] += 1

   

print(Ones)

print(Boths)

 

#     X軸上のグリッド点100

x_coord = np.linspace(As.min(), As.max(), 100)

 

def Pd(u):

    return 1.0 / (1.0 + np.exp(-u))

 

def Pc(x):

    return Pd(x) + (1.0 - Pd(x)) / 3

 

y_Pc_m = Pc(a_mode + b_mode*x_coord)

y_Pd_m = Pd(a_mode + b_mode*x_coord)

plt.plot(x_coord, y_Pc_m, label = 'Pc')

plt.plot(x_coord, y_Pd_m, label = 'Pd')

#

#     各カテゴリ区間におけるデータの正答率

#

data_x = [(v - As_mean)/As_sd for v in [25, 35, 45, 55] ]

data_y = [Ones[i]/Boths[i] for i in range(4)]

plt.plot(data_x, data_y, 'o', label = 'Data')

 

x_vs = [(v - As_mean)/As_sd for v in [20, 30, 40, 50, 60]]

x_lbls = [f'{v}' for v in [20, 30, 40, 50, 60]]

plt.xticks(x_vs, x_lbls)

plt.xlabel('独立変数', fontsize = 14)

plt.legend(fontsize = 14)

plt.ylabel('確率', fontsize = 14)

plt.title('3点識別試験', fontsize = 16)

plt.savefig('FigRegAge.png')

plt.show()

 

 

Up