hidekatsu-izuno 日々の記録

プログラミング、経済政策など伊津野英克が興味あることについて適当に語ります(旧サイト:A.R.N [日記])

Python で主成分分析(PCA)する

工数の分析にあたり、画面、帳票、バッチの重み付けをシステマティックに行いたいという積年の課題があり、単純に考えると重回帰分析を行えばという話なのだけど、実際にやってみるとまったく予想外の結果がでる。

df= pd.read_csv('./IPA_2014-2019.csv')
model = smf.ols('MH ~ VC + RC + BC - 1', df)
result = model.fit()
result.summary()
      coef        std err   t         P>|t|    [0.025     0.975]
VC    276.7189    23.494    11.778    0.000    230.532    322.906
RC    173.3967    49.852     3.478    0.001     75.391    271.403
BC    -15.2673     9.259    -1.649    0.100    -33.469      2.934

なんとバッチの係数がマイナス! これを信じて工数計算を行うとバッチ本数を大量に増やすことでゼロカロリーならぬゼロ工数を実現できてしまう。*1

色々考えていたところ、uncorrelated 氏に「多重共線性が出ているので、何か説明変数を落とすか、主成分分析で変数を合成するなりした方が良さそう」との助言を頂いた。

ようは変数間の相関が無視できないということだ。この事自体は前から気付いていたのだけど、説明変数を落とすのは工数を決めるための式を求めるそもそもの目的にそぐわないし、主成分分析は軸を変えてフィッティングするという程度しか知識がなく画面、帳票、バッチの数値からどうやって軸を変えて工数を算出すればいいのかよくわからなかったので、手を出すのを躊躇していたのであった。

助言をもらったからには重い腰を上げてやってみようということで、今回のエントリはその作業メモ。

まず、主成分分析だけど基本的には、元の説明変数の軸を変換してよりフィットしやすい説明変数を作り出そうというもの。単に説明変数を作り出すだけだとあまり意味はないけれども、作り出された説明変数を落とすことで、元の説明変数すべての影響を残すことができる。一般的には累積寄与率が7、8割を超えるところまで残すのがよいとされているようだ。

Python で主成分分析をする方法を調べてみると、statsmodels を使う方法と scikit-learn を使う方法があることがわかった。ただ、statsmodels の方はあまりこなれていない印象で、scikit-learn を使う方がよさそうだ。

scikit-learn で主成分分析

from sklearn.decomposition import PCA
df = pd.read_csv('./IPA_2014-2019.csv')
X = df[["VC", "RC", "BC"]].values

pca = PCA()
pca.fit(X)

結果は、pca オブジェクト内に保持される。まず、寄与率を調べる。寄与率は pca.explained_variance_ratio_ で取得できる。

array([0.85622803, 0.11874323, 0.02502874])

結果を見る限り、最初の要素だけで8割超えているので、第1主成分だけでほとんど説明できると解釈できる(本当か?)。そこで、第一主成分のみで回帰を行う。

df2 = pd.DataFrame(columns=["C1", "C2", "C3"], data=pca.transform(X))
df2["MH"] = df["MH"]
model = smf.ols('MH ~ C1 - 1', df2) # C2、C3 は捨てる
ols = model.fit()

回帰分析の結果は、ols.params で取得でき、C1の係数 21.47954 だった。

これを画面、帳票、バッチ数からなる式にする必要がある。transform は、(元データの行列(X) - 元データの列ごとの平均(np.mean(X, axis=0))) × 主成分行列(pca.components_) という計算を行っているので、同じことを行って主成分に変換すればいい。

今回のデータの場合、最終的には次の結果が得られる。

人時工数 = 2.50 * 画面数 + 0.62 * 帳票数 + 21.32 * バッチ数 - 1459.15

見れば明らかだけれど、この結果も妥当とは言いがたい。係数がマイナスなのもそうだが、画面、帳票、バッチの比率も明らかにおかしい。これは線形回帰、主成分分析に共通する特徴として、外れ値に弱いことが原因と考えられる。

この問題に対応するためにロバストPCAという方法があるものの、scikit-learn にも statsmodels にも組み込まれておらず、野良ライブラリの dganguli/robust-pca を使う必要があるようだ(現段階では調査中)。

[追記] robust-pca を試してみたが、ほとんど外れ値として扱われてしまい元の行列とは似てもつかないものになってしまった。

以下の結果の S が外れ値、Lが外れ値を考慮した行列

import libs.r_pca as r_pca
pca3 = r_pca.R_pca(D=df[["VC", "RC", "BC"]])
L, S = pca3.fit()

statsmodels で主成分分析

statsmodels では次の方法で実行できるが、なぜか scikit-learn での結果とまったく一致せず断念してしまった。オプションの違いなのだろうか……

import statsmodels.multivariate.pca as pca
df = pd.read_csv('./IPA_2014-2019.csv')
result = pca.PCA(df[["VC", "RC", "BC"]])

*1:例によって、データはIPAソフトウェア開発データ白書のものを加工して利用。

PyMC3 を使ったベイズ一般化線形モデル回帰分析メモ

久々にIPAソフトウェア開発データ白書のデータを分析してみようと思い、ベイズ推定を使ってみたのだけど、データの分散が大きすぎるので分位点回帰みたいな方法使わないとだめだということがわかったのでメモだけ。

インストール

環境は Windows 10 の WSL2 Ubuntu 20.04 LTS。WSL2 は本当に便利。

sudo apt install python3 python3-pip python3-pandas jupyter-notebook
pip3 install pymc3

WARNING (theano.tensor.blas): Using NumPy C-API based implementation for BLAS functions. という Waning が出る場合は、MKL という NumPy を高速化する Intel 謹製ライブラリをインストールすると消える。

pip3 install mkl

ライブラリとデータの読み込み

ここは例によって例の通り。以下、y、x1、x2 の三列がある前提で書いている。

import pandas as pd
import matplotlib.pyplot as plt
import pymc3 as pm

indata = pd.read_csv('./indata.csv') # y, x1, x2

ベイズGLM

GLM用の便利関数が用意されているし、formula も使えるので一見簡単に使える。 これが実際には、いろいろと初期設定を変える必要があり結構詰まる。

with pm.Model() as model:
    pm.glm.GLM.from_formula('y ~ x1 + x2', indata)
    trace = pm.sample(draws=5000, chains=4, cores=1, start=pm.find_MAP(), step=pm.NUTS())

formula は indata の中の変数名を使える。y ~ x なら単回帰、y ~ x1 + x2 なら重回帰、切片はデフォルトであり扱いなので不要な場合は y ~ x1 + x2 -1 のように -1 をつけると切片=0になる。

draws はサンプルの生成回数。chains は生成するサンプル列の数。ベイズ推定の場合、MCMCを使うためいつも異なる結果を返すので、複数回実行して安定した結果を得られているか確認することができる。cores は並列実行数。

start は、初期の事前分布を設定するもので、find_MAP は MAP推定値を設定するものらしい。正しい使い方なのかはよくわからない。

step は、サンプルを生成するサンプラー。2値の場合は BinaryMetropolis、離散値の場合は Metropolis、連続値の場合は NUTS を使うと良いらしい。

from_formula メソッドの引数に family でリンク関数を priors を付けることで説明変数の事前分布を変えることができる。family に指定できるリンク関数は、Normal(正規分布)、StudentT(スチューデントのt分布)、Binomial(2項分布)、Poisson(ポアソン分布), NegativeBinomial (負の2項分布)の5つ。priors で使える分布はここで説明されているものが利用できる。後者はグラフとともに説明されているので大変わかりやすい。

    pm.glm.GLM.from_formula('y ~ x1 + x2', indata, 
         family = pm.glm.families.Normal(),
         priors = {
            'x1': pm.Normal.dist(mu=0., sigma=1.),
            'x2': pm.Normal.dist(mu=0., sigma=1.),
            'Intercept': pm.Normal.dist(mu=0., sigma=1.), // 切片は Intercept
        }
    )

trace にはサンプリングの結果が入っている。この結果は PyMC3 付属のプロット系関数でグラフ化して確認できる

pm.traceplot(trace[4000:]);
pm.summary(trace[4000:])
pm.plot_posterior(trace[4000:]);

trace[4000:] と書いているのは、5000回サンプルを生成したうち、安定したであろう 4000回以降を使うという意味。

traceplot でデータが収束しているかを判断できる。左側のグラフは同じ形で重なっている、右側は特定の値周辺に散らばっていれば、収束しているとみなしていい。summary の r_hat が 1.1 以下という判断でも良いらしい。

summary は各値の詳細、plot_posterior は summary 内容を可視化したもの。

続・Javaのクラス内が依存しているフィールド、メソッドの一覧を表示する (Java11 対応版)

大昔、「Javaのクラス内が依存しているフィールド、メソッドの一覧を表示する」という記事を書いたのだが、久々に使ってみるとどうも一部のメソッド呼び出しが抜けている。いろいろ調べていたのだが、なるほど invoke dynamic の仕業だった。

なんせ前回の記事を書いたのが 2013 年だから 7 年前か。無駄に歳を重ねてしまったものだ。おそらく当時は Java 1.7 だった……と考えるとそれほど変わったわけでもないな。1.6 と 1.8 の時代が大変長かったし、世の中的にはまだまだ Java 8 が現役だ。

閑話休題invoke dynamic 命令自体は Java 1.7 の頃から存在はしたものの、コンパイラが生成したコードに含まれるようになったのは Java 8 からで、主にラムダ式に使われている。そのため、前回の記事の時点ではあのコードで十分だったのだが、Java 8 以降で使うと漏れが発生してしまうわけだ。

そういうわけで、Javassistinvoke dynamic を扱う方法を調べていたのだが、これが意外とやっかいで困ってしまった。invoke dynamic 命令自体は、クラスの属性として定義されている Bootstrap Methods 属性に定義されている Bootstrap Method 配列の番号しか持っていない。そして Bootstrap Method は、実際のメソッドの呼び出しをラッピングするメソッド ハンドルを持っている。結局、invoke dynamic 命令⇒Bootstrap Methods 属性⇒Bootstrap Method 配列のひとつ⇒メソッド ハンドル⇒実際のメソッドと4回たどる必要があった。

最終的なソースコードは以下の通り。Bootstrap Method を取得するところと invoke virtual の前処理を除けば前回から大きな違いはない。

ClassPool cp = ClassPool.getDefault();
CtClass cc = cp.get(クラス名);

BootstrapMethod[] bsms = null;
for (AttributeInfo attr : cc.getClassFile().getAttributes()) {
    if (attr instanceof BootstrapMethodsAttribute) {
        BootstrapMethodsAttribute bsma = (BootstrapMethodsAttribute)attr;
        bsms = bsma.getMethods();
    }
}

for (CtBehavior cb : cc.getDeclaredBehaviors()) {
    MethodInfo info = cb.getMethodInfo2();
    ConstPool pool = info.getConstPool();
    CodeAttribute code = info.getCodeAttribute();

    if (code == null)
        return;

    CodeIterator i = code.iterator();
    while (i.hasNext()) {
        int pos = i.next();
        int opecode = i.byteAt(pos);
        int dynIndex = -1;
        if (opecode == Opcode.INVOKEDYNAMIC) {
            int index = i.u16bitAt(pos + 1);
            int bootstrap = pool.getInvokeDynamicBootstrap(index);
            BootstrapMethod bm = bsms[bootstrap];

            for (int arg : bm.arguments) {
                int tag = pool.getTag(arg);
                if (tag == ConstPool.CONST_MethodHandle) {
                    switch (pool.getMethodHandleKind(arg)) {
                        case ConstPool.REF_invokeVirtual:
                            opecode = Opcode.INVOKEVIRTUAL;
                            dynIndex = pool.getMethodHandleIndex(arg);
                            break;
                        case ConstPool.REF_invokeSpecial:
                            opecode = Opcode.INVOKESPECIAL;
                            dynIndex = pool.getMethodHandleIndex(arg);
                            break;
                        case ConstPool.REF_invokeStatic:
                            opecode = Opcode.INVOKESTATIC;
                            dynIndex = pool.getMethodHandleIndex(arg);
                            break;
                        case ConstPool.REF_invokeInterface:
                            opecode = Opcode.INVOKEINTERFACE;
                            dynIndex = pool.getMethodHandleIndex(arg);
                            break;
                    }
                    break;
                }
            }
        }

        switch (opecode) {
        case Opcode.NEW:
        case Opcode.ANEWARRAY:
        case Opcode.MULTIANEWARRAY: {
            int index = i.u16bitAt(pos + 1);
            
            System.out.print(cc.getName());
            System.out.print('\t');
            System.out.print(cb.getLongName());
            System.out.print('\t');
            System.out.print("new");
            System.out.print('\t');
            
            String className = pool.getClassInfo(index);
            if (opecode == Opcode.ANEWARRAY || opecode == Opcode.MULTIANEWARRAY) {
                className = "new " + className + "[]";
            }
            
            System.out.print(className);
            System.out.println();
            break;
        }
        case Opcode.NEWARRAY: {
            System.out.print(cc.getName());
            System.out.print('\t');
            System.out.print(cb.getLongName());
            System.out.print('\t');
            System.out.print("new");
            System.out.print('\t');
            
            String className = null;
            switch (i.byteAt(pos + 1)) {
            case Opcode.T_BOOLEAN: className = "new boolean[]"; break;
            case Opcode.T_CHAR: className = "new char[]"; break;
            case Opcode.T_BYTE: className = "new byte[]"; break;
            case Opcode.T_SHORT: className = "new short[]"; break;
            case Opcode.T_INT: className = "new int[]"; break;
            case Opcode.T_LONG: className = "new long[]"; break;
            case Opcode.T_FLOAT: className = "new float[]"; break;
            case Opcode.T_DOUBLE: className = "new double[]"; break;
            }
            
            System.out.print(className);
            System.out.println();
            break;
        }
        case Opcode.CHECKCAST: {
            int index = i.u16bitAt(pos + 1);
            
            System.out.print(cc.getName());
            System.out.print('\t');
            System.out.print(cb.getLongName());
            System.out.print('\t');
            System.out.print("cast");
            System.out.print('\t');
            
            String className = "(" + pool.getClassInfo(index) + ")";
            
            System.out.print(className);
            System.out.println();
            break;
        }
        case Opcode.GETSTATIC:
        case Opcode.PUTSTATIC:
        case Opcode.GETFIELD:
        case Opcode.PUTFIELD: {
            int index = i.u16bitAt(pos + 1);
            
            System.out.print(cc.getName());
            System.out.print('\t');
            System.out.print(cb.getLongName());
            System.out.print('\t');
            System.out.print("field");
            
            String className = pool.getFieldrefClassName(index);
            String fieldName = pool.getFieldrefName(index);
            String desc = pool.getFieldrefType(index);
            String typeName = Descriptor.toClassName(desc);
            
            System.out.print('\t');
            System.out.print(typeName);
            System.out.print(' ');
            System.out.print(className);
            System.out.print('.');
            System.out.print(fieldName);
            System.out.println();
            
            break;
        }
        case Opcode.INVOKEVIRTUAL:
        case Opcode.INVOKESPECIAL:
        case Opcode.INVOKESTATIC:
        case Opcode.INVOKEINTERFACE: {
            int index = dynIndex == -1 ? i.u16bitAt(pos + 1) : dynIndex;
            
            System.out.print(cc.getName());
            System.out.print('\t');
            System.out.print(cb.getLongName());
            System.out.print('\t');
            System.out.print("method");
            
            String className;
            if (opecode == Opcode.INVOKEINTERFACE) {
                className = pool.getInterfaceMethodrefClassName(index);
            } else {
                className = pool.getMethodrefClassName(index);
            }
            
            String methodName;
            if (opecode == Opcode.INVOKEINTERFACE) {
                methodName = pool.getInterfaceMethodrefName(index);
            } else {
                methodName = pool.getMethodrefName(index);
            }
            
            String desc;
            if (opecode == Opcode.INVOKEINTERFACE) {
                desc = pool.getInterfaceMethodrefType(index);
            } else {
                desc = pool.getMethodrefType(index);
            }

            String ret = null;
            List<String> params = new ArrayList<String>();
            if (desc.charAt(0) == '(') {
                int start = 1;
                boolean inClass = false;
                for (int j = start; j < desc.length(); j++) {
                    char c = desc.charAt(j);
                    if (inClass) {
                        if (c == ';') {
                            params.add(Descriptor.toClassName(desc.substring(start, j + 1)));
                            start = j + 1;
                            inClass = false;
                        }
                    } else if (c == ')') {
                        ret = Descriptor.toClassName(desc.substring(j + 1));
                        break;
                    } else if (c == 'L') {
                        inClass = true;
                    } else if (c != '[') {
                        params.add(Descriptor.toClassName(desc.substring(start, j + 1)));
                        start = j + 1;
                    }
                }
            } else {
                ret = Descriptor.toClassName(desc);
            }
            
            System.out.print('\t');
            System.out.print(ret);
            System.out.print(' ');
            System.out.print(className);
            System.out.print('.');
            System.out.print(methodName);
            System.out.print('(');
            for (int j = 0; j < params.size(); j++) {
                if (j > 0) System.out.print(',');
                System.out.print(params.get(j));
            }
            System.out.println(')');
            break;
        }
        }
    }
}

nuxt-history-state という Nuxt.js 用モジュールをリリースした

仕事で Nuxt.js を使っている。戻るボタンの扱いについて聞かれたので調べてみたところ、これが予想外に難しい。

戻るボタン対応は、Struts を代表とするポストバック系のフレームワークでも鬼門だったけれど、SPAの時代になってもあいかわらず鬼門のようだ。

Backボタンを押した際のブラウザ動作としては、タブ切り替えと同じ挙動の方がありがたいのではと思うのだけど、残念ながらそうはならない。元々、静的ページを前提にした仕組みだから、以前訪れたページがリロードされるような動きとなる。ただし、HTMLにはフォームというものがあるから、そこだけは考慮されていて、キャッシュが残っていればフォームの値だけは復元、なければ完全リロードとなかなかに際どい動きとなる。

もちろん、この動作はJavaScript全盛の今となっては好ましいものではない。フォームの値だけ残されたところで JavaScript のメモリ状態と整合性が取れず中途半端になってしまう。とはいえ、onunload など既存仕様と整合性が取れないことやメモリ状態も含めてキャッシュに保管するのは現実的ではないなど様々な理由が考えられ、この動作が変わることはないだろう。

Nuxt.js に関して言うと、すべての状態が JavaScript 管理となるため、フォームの値だけが残されるという中途半端な状態にはならない。$router.push による画面遷移とさして変わらない動作をする。逆に言えば、一般の静的ページで期待される動作を再現するためには工夫が必要になる、ということでもある。

きっと誰かがプラグインを作っているだろうと高をくくっていたのだけれど、意外にもこれといったものがない。たしかに多くの Web サービスでは、状態保持が必要な画面遷移もあまりないし、細かいことを気にしなければ Vuex で十分かもしれない。

とはいえ、私が仕事で関わるような業務系アプリケーションの場合、状態を伴う画面遷移の方がむしろ一般的なため、何らかの対応が必要となる。本来、仕事の一環なのだから業務時間中に作ればいいとは思うのだが、開発に専念できない現状で片手間にできるような内容ではないし、今後、別案件や個人的な開発にも流用したいことから、プライベートな時間を使ってモジュールを作成した。*1

そうして出来上がったのがこの nuxt-history-state だ。

このモジュールを作るにあたっては、Vue-Router の scrollBehavior と「vue-routerでbackwardかforwardかを判定する - Qiita」が参考になった。

基本的な方針としては、各ページにページ番号を持ち、画面遷移時にインスタンスの$data をページ番号に紐付け保管するという対応になる。ページ番号はブラウザ内で管理される履歴と紐付ける必要があるため、URL のクエリパラメータか history.pushState のいずれかで保持する必要がある。

クエリパラメータで紐付ける方が簡単なのはわかっていたが、URLが汚れる点が気になった。だから、history.pushState で何とかならないかと努力してみたものの、リロード後に Back ボタンを押した場合、popstate イベントが発生しない場合があることが判明したため、デフォルト動作ではリロードをサポートせず、オプションを設定することでクエリパラメータを使ったリロード対応モードになるようにした。

 このモジュールを使うと、通常は難しい Back/Forward の判定やリロード後の Back/Forward 継続など、履歴管理が一通りできるようになる。まだ、開発して間もないので、こうした方が良いのではなど意見があれば、Issue に起票してほしい。

*1:最近、NGINXの開発者が当時在籍していた会社からの著作権侵害報告で強制捜査を受けたというニュースが出たこともあり、センシティブな話かもしれないので、プライベートな時間を使って開発されたことを強調しておこう。私が公開しているライブラリは、NGINXがそうであったように、いずれも業務時間で開発したものではなく、プライベートな時間で開発したものを公開し、職場からは一般のライブラリと同じ形で利用するようにしている。いやほんと、業務時間中にOSS開発できる会社が羨ましいよ!

JEF4J v0.9.0 をリリースした。そして謎の文字の話。

JEF4J を久々に更新した。

以前、「JEF4J をリリースしてみた。あるいは、メインフレームの文字コードの話。」というエントリを書いたが、その時点では JEF の最終仕様が掲載されている「FACOM JEF 文字コード索引辞書 (1987年/第三版)」が手に入らなかったため、第二版をベースに他資料を援用して作成していた。

福岡大学の図書館に存在していることはわかっていたのだけれども、さすがに遠方で放置してたのだ。別件で福岡に旅行に来ているので、ついでにコピーを入手してきた。

いろんな文字が追加されているのではという予想を裏切り、JIS83 改正対応がほとんどだったようで、結果的に修正はほぼ必要なかった。

残る謎文字

ワープロ用途の特殊記号や同じ字母を持つものが登録済みの変体仮名を除くと Unicodeマッピングできない文字は次のものだけとなった。

JEF 字形 備考
71E0 武を字母とする変体仮名Unicode には存在せず。
71F7 変体仮名なのだが、字母すら不明。
72A8 得を字母とする変体仮名Unicode には存在せず。
72E1 女を字母とする変体仮名Unicode には存在せず。
73D5 富を字母とする変体仮名Unicode には存在せず。
74B9 変を字母とする変体仮名Unicode には存在せず。
75F2 雄を字母とする変体仮名Unicode には存在せず。
43C7 儲の異体字。違いが微妙すぎるのか、文字情報基盤にも同一字体なし。
6CF4 顛の異体字。違いが微妙すぎるのか、文字情報基盤にも同一字体なし。

マッピングして気づくのは、Unicode 登録の変体仮名はかなり中途半端な代物なのでは、という点だ。JEF が変体仮名をサポートしていたのなら、既存規格に合わせた方がまだ良かったように思える。とはいえ 71F7 の変体仮名については、それ系のサイトをいくつか調べてみたものの該当する文字すら見当たらず本当に謎なのではあるが。

儲、顛の異体字については、JEF79 で採用していた拡張漢字とJIS83 で採用された文字に微妙に差異があったため発生したもので、互換文字みたいなものだと思われる。

今後の予定

文字のマッピングアルゴリズムに多少改善の余地があるのと、Adobe Japan-1 対応を入れる可能性がある。ノー豆腐を目指した Noto Font を含め多くのフォントは Adobe Japan-1 の異体字セレクタはサポートしても、汎用電子の異体字セレクタはサポートしていない。

正直、あまりにもマイナーすぎてそんなに使われているとも思えないライブラリだけれども、今後発生するだろう公共系システムの移行に役立つ局面もあるとは思っているので、要望があればご連絡いただければと。

AWS 認定 Solution Architect - Professional に合格した

41歳で自動車免許を取ったというエントリを前回書いたけれども、実は並行して AWS の Solution Architect Professional の試験勉強をしていた。免許で長期休暇を取ったこともあり、受験のタイミングに難儀していたのだが、先日ようやく受けることができ、無事合格できた。点数はわりとギリギリだったけど、この試験は答えが明らかではない際どい問題が多いのでこんなもんかな、と。*1

AWS の試験は、AWS を理解するきっかけづくりに最適だと思う。特に Solution Architect - Associate は教本も増えてきており、一週間ほどで AWS の主要な要素が理解できるようになっているのでおすすめできる。Solution Architect - Professional は範囲が広いわりに教本らしい教本もなく、試験内容自体の難易度も高いが、こんな機能もあるんだという気付きに繋がり個人的には非常に役に立った。一点残念なのは、試験の模範解答が手に入らないところ。受験対策だから仕方ないとは思うものの、誤った理解を正す機会が得られないのは残念だ。

なお、勉強は Udemy で講義と模擬試験の英語版を購入して実施した。だいたい1万円強だったか。受験費用も模試含め 2万円ほどかかる*2ので、会社から合格後の報奨金が出なければ絶対に受験しなかったと思う。

仕事では AWSGCP、Azure、そしてオンプレと広く薄く雑多に関わっているのだが、使ってみると、それぞれ会社のカラーが出ていて面白い。

AWS は、質実剛健というか、マイクロサービス的な提供がされていて、簡単なことでも複数のサービスを組み合わせないと実現できないため、AWS特有の広範な知識を求められる難しさがある。一方で、組み合わせれば大抵のことはできるようになっているのも事実であり、痒いところに手が届くという感覚がある。ここは不便だなと感じると、すかさず新サービスでフォローされることが多く、そういう意味でも安心感がある。

GCP は、シンプルで高品質な感じがある。3クラウドの中では一番わかりやすいし、ひとつひとつのサービスの出来が良いように感じる。一方でシンプルすぎて痒いところに手が届かない感じがある。例えば、CloudSQL と Spanner の中間がないところとか。Spanner はすごいけど、欲しいのは Amazon Aurora なんだけど的な。

Azure は、コンソールのインターフェイスはよくできている。さすがは UI の会社だけある*3。とはいえ、ひとつのサービスにいろんな機能がごちゃっと付いてきたり、サービス間の関連がわかりずらかったり、新機能にもとってつけた感があったりと。どこがとはっきり言えるわけではないのだが、やっつけ感を強く感じる。良くも悪くも Microsoft 製品っぽいというか。

まぁ、三者三様とは言っても、どのクラウドを採用するか迷っている会社があったら、個人的には AWS を押す。最安値での環境構築はできないかもしれないけど、やりたいことはだいたいできるという安心感は大きい。この機能以外はいらない、ということが確実なら GCP を使うのは全然ありだと思う。Azure は……多くを語るまい。

大企業の基幹系の仕事が多いから、いまだオンプレに触れる機会も多いけど、個人的にはもはやオンプレはリスク以外の何物でもないかな、と。好きな時にリソースを増減させられないということがいかに行動を制約するかを考えたら、一見金額が安く見えたとしてもオンプレにするという選択は難しいように思う。特にディスクについては、自由に増加可能なクラウドと事前購入が必須なオンプレでは見積り以上に差が出てくるのではと。

 

*1:実際、海外の対策サイトを見に行くと過去問の回答に対する意見が割れている場面をしばしば見かける。

*2:本当は 3万円以上かかるが、Associate を取得すると半額チケットが貰える

*3:というより AWS Console がわかりづらすぎなだけなんだけど

41歳にして自動車免許を取った

孔子曰く、「40歳にして惑わず」とは言うものの、40歳を超えたくらいで、そうそう人間変わらない。うんざりするほど子供っぽい(余談だが、仮面ライダージオウはやたら面白い)し、むしろ、大人にならなきゃ、という抑圧から解放され、より幼くなっているのではと思うこともある。

感覚的にはそんな感じなのだけど、一方で、せいぜい40年しか人生残っていない、という事実にも気付かずにはいられない。そんな感覚は30代にはなかった。今までは、いつかやればいいや、と思っていたのだが、もはや今やらないと一生やらないことが確定してしまうのだ。

そこで自動車免許だ。

正直、自動車免許が絶対に必要だと思ったことは一度もない。周りの話を聞くと、身分証明書として便利という声が多いのだけど、写真付きのマイナンバーカードで十分なので住基カードより前の知識で止まっているのだろう。*1

東京暮らしが人生の半分を過ぎた今、車に乗る必然性はまるでない。せいぜい、旅先で自動車乗れたら便利だろうな、と思うくらいか。とはいえ、このまま車を運転することなく死んでいくのか、と思うと怖くなってきた。車に乗る必然性はないとはいえ、子供の時はゴーカートが好きだったし、レースゲームも大好物だ。それ以上に、日本人の通過儀礼としての自動車免許を取らなかったことが、自分の幼さに繋がっているのかもしれないという思いもなくはない。

そこで関係各所と調整のうえ、なんとか2週間の休暇をもらい、6月~7月にかけて、福島の湯本自動車学校という場所に合宿免許に行ってきた。

40代で自動車免許を取る人も珍しかろうから、どんな感じだったかを簡単に書いておこう。

  • 申し込みは「ローソンの運転免許」で行った。トラブルがあったとき大手の方が仲介に入ってもらいやすいかもしれない、という思いでそうしたが、申し込んで振り込みしたらすんなり終わった。資料が送られてくるので、その指示に従がえば良いだけで特に迷うようなところはなかった。
  • 費用的にはAT限定免許で20万円強だった。東京で免許を取ると、合宿でなくても30万円を超えるので、地方で合宿免許というのはとても経済的。宿舎は予想外に快適だったし、飯もうまかった(お昼の弁当はかなりいまいちだが)。お風呂は宿舎含め温泉で、日帰り入浴にもあちこち行った。湯本温泉は古いせいか露天風呂が少なくてそこが残念ではあった。
  • 半分は夏休み気分で行った(だから温泉地にした)のだが、休日なしの9:00~19:30でスケジュールが組まれているため、休暇感はまったくなかった(途中に空き時間はそこそこあるとはいえ)。特に仮免の見極めに落ちた辺りで、休暇の延長が必要になり、かなり焦ることになってしまった。
  • AT免許では、第一段階で技能教習が最短12時間となっているが、まったくの素人が乗れるようになるには短すぎるように感じたので、数日、日程が伸びることは覚悟した方がよいと思う(MTだと15時間だが、このくらいの時間が最低ラインではなかろうか)。
  • 車の運転が思っていたものとまったく違っていた。もっとシステマチックなものかと思っていたのだが、ありえないくらいメカニカルだ。ハンドルやアクセル/ブレーキの使い方もレースゲームとまったく違うので当初すごく戸惑った。
  • 昨今、ブレーキとアクセルの踏み間違い事故が話題になることが多いが、この UI なら起こって当然ではと思ってしまった。ブレーキも最大限押し込むと徐行速度になって、離せば止まるとかになっていれば、そんな事故起こらないのに。ほんと、テスラがんばれ、と言いたい。
  • 結局最後まで、適切な速度、安全確認の手順、車体間隔がわからなかった。ダメ出しばかりで何が正解なのか良くわからない挙げ句、教官によって言うことがぜんぜん違ったりするので、とても戸惑った。教育方針のすり合わせとかしないもんなのか? 速度に関しては、標識がないから60kmと言う教官もいたのだが、調べる限り、片側1車線なら50km、片側2車線なら60km が上限となっているそうだ*2。教科書にすら載っていないが。
  • 車体の間隔に関しては実際に降車して体験するなどの講習があった方がよいのではないか。感覚だけで覚えるには時間数が短すぎるように思う。本線合流に至ってはまったく事前説明がなく、いくらなんでも無理があるだろ、という感想しかない。
  • これだけ多くの人が免許持っている割に、合理的とは言い難い制度と仕組みで構成されており、世の中想像以上に適当なものだと実感した。
  • 正直、取得には大変苦労した。学科試験も決して簡単なものではなく、よくこんな大変な試験を世間の大半が通過してるもんだと思った。これに合格できるなら日商簿記とか情報処理試験とか余裕で合格するはずなので、みんなもっとまじめに資格試験受けた方がよいのではないか、という気持ちになった。

本当は40歳のうちに免許を取ってしまいたかったのだが、本免学科試験は居住地でないと駄目とのことで、最終的には誕生日を過ぎた7月後半に免許発行ということになった。とはいえ、免許の有効期限は誕生日が基準となるので、結果オーライと言えなくもない。

いまだ車線変更とバックでの駐車がうまくできないなど、まだまだ初心者を脱しきれていないので、しばらくは週に一度くらいは乗ろうかな、という感じ。

*1:マイナンバーカードはコピーできる条件に制限があり、そのせいで身分証明書としての使用を拒否するお店があるのがやや面倒ではある。専用ケースに入れた状態での表面ならば、身分証明書として使えることになっているのだけど。その意味で住基カードより利便性が落ちているのは残念だ。

*2:実際には交通量などによってもっと細かい規定がある。