技術メモ

役に立てる技術的な何か、時々自分用の覚書。幅広く色々なことに興味があります。

統計検定1級に合格しました

以前統計応用については合格していたのですが、統計数理は何度か落ちていたためN度目のリベンジでした。この度2022年度統計数理も合格することができたのではれて統計検定1級を名乗れるようになりました。
月並みですが、自分なりに対策したことを書き残しておこうと思います。

理論

統計検定1級の問題は非常に質がよく、理論をきちんと追って理解していれば何を聞きたいのかがなんとなくわかるようになっています。
むやみな計算問題ではなく問題の背景があるような問題が多いです。
その点でも、理論は使えるだけでなくきちんと体系立てて理解しておく必要があると思います。

一言で理論を抑えると言ってもその範囲は多岐にわたるので、ここでは少なくとも最低限絶対に抑えておかないといけないと思うポイントを挙げておきます。

確率分布
  • 確率変数の変数変換・合成

畳み込み積分を使った単純な和、ヤコビアンを使った確率変数の和・積・商、確率母関数を使った確率変数の和を身体に染み込むまで演習しましょう。

  • 確率分布の変形

よく言われますが、いわゆる確率分布曼荼羅を写経して理解して書けるようにしましょう。自然とガンマ分布は何も見なくても書けるようになります。

  • 確率分布の期待値・分散

確率母関数による期待値の計算方法など、代表的な確率分布の期待値と分散は計算できないと話になりません。

定量の計算および検定量の評価
  • クラメールラオの不等式の導出

フィッシャー情報量の導出と呼ぶべきか、不偏推定量の分散の下限の導出です。

  • 最尤法

最尤法は基本的な内容ですが頻出です。最尤推定量を求めさせた上で不偏性を論じたり他の統計量と比較させたりするような問題が定番です。

よくあるのが不偏推定量であることの証明、最尤推定量の分散を求めさせるなどは頻出です。

検定論
  • ネイマンピアソンの補題の証明

問題として直接出題されることはありませんが、問題背景になることもありますし、統計のスペシャリストを名乗るなら抑えておくべきでしょう。

  • 尤度比検定・ワルド検定・スコア検定・適合度検定

適合度検定も何かと出題されます。特に尤度比検定はネイマンピアソンの補題の土台になっているので大事です。

  • z検定・F検定・t検定

統計応用につながる話ですが、できれば厳密な証明はなくともネイマンピアソンの補題から導出される流れは抑えておきたいです。

  • 分散分析

分散分析っぽい問題も統計数理で出題されます。

その他

符号検定・マンホイットニーのU検定・ウィルコクソンの順位和検定(できれば導出 )など、
一度符号検定の導出の問題が出題されました。必ずしも導出できる必要はありませんが、導出できて損はないと思います。

  • 近似論

デルタ法、正規近似とその精度など、細かい話は過去問を解きながら補う感じで良いと思います。

教材

最初に挑んだ時は何もわからず公式教科書を使用していましたが、
正直公式教科書はわかっている人がそうそうそうだよねと確認するために読むものです。
初学者が読むものではありません。もう一度記憶をリセットして勉強し直すなら副読本として買いはしますが、この本の通りに勉強しようとはなりません。とはいえ、公式が出しているので内容に過不足がないという点ではすごく頼もしいです。

公式教科書で失敗した経験を生かして教科書選びは慎重に自分にあったものを選ぶようにしました。
その結果メイン教材としては竹村本に落ち着きました。統計検定1級の対策本としては比較的有名な本です。

合う合わないは人によりますが、自分にとってはちょうどよい難しさで肌にあっていました。

副教材として以下を使用しました

こちらは上の竹村本より易しめです。意外と使っているという人はいませんが、程よく証明も載っていて良書だと思います。竹村本でよくわからないところを補助的に読む感じで使っていました。こちらは上の竹村本より難しめです。統計検定の教材として最も有名な本ではないでしょうか。竹村本で証明が飛ばされていたりもやもやするところを深堀りする形で使っていました。付属の演習問題を解くのが統計検定対策として一般的らしいですが、解説が不十分なため自分は使いませでした。


あとはひたすら過去問でした。
何度か落ちているので当然といえば当然ですが6年分は過去問を解きました。

ただ、統計検定1級について苦労したこととして、解説を読んでもよくわからない・1つの問題が解けても他に応用することができない(過学習してしまう)という悩みがあります。正直合格した今でもこの悩みはどうすればいいのか正解はわかりません、自分自身過去問に過学習してしまっているという自覚があり統計のスペシャリストと名乗るにはおこがましいと感じていますが、今後も研鑽していこうと思っています。

Flaskを使ったOAuth2.0のクライアント側の実装

手を動かしてOAuthを理解したかったので、お試しクライアントアプリを書いてみた時の備忘録。
Flaskで実装されている例が見当たらなかったのでちょうどよいと思って試してみた。
(python3.10.9)

OAuth2.0とは

OAuthとは一言で言えば、利用者に外部のサービスを使う(Gmailの新着メールを見る、Twitterのタイムラインを見る)ための許可を安全に得るための技術である。
仕様の流れは以下の通り。
登場人物はサービス利用者・サーバー・外部サービス認可サーバー・外部サービスリソースサーバー。
①サーバーは外部サービス認可サーバーにリダイレクトさせ、サービス利用者にログイン情報を入力してもらう
②ログイン情報が正しければ、外部サービス認可サーバーはサーバーにアクセストークンを発行する
③サーバーがアクセストークンを使って外部サービスリソースサーバーのリソースを使用する

https://learn.microsoft.com/ja-jp/azure/active-directory/fundamentals/media/authentication-patterns/oauth.png
*1

似たような技術にOpenID Connectがある。
OpenID Connectはリソースの使用許可を取るというよりID管理を外部サービスに委ねるための技術である。
GoogleアカウントでYoutubeにログインする、Facebookアカウントでゲームにログインする)

シングルサインオンとも少し似ているが、
シングルサインオンは複数のサービスを一元的に管理するための技術である。
(社内の認可サーバーを一度通ればSalesforceにもMicrosoftにもサインインする)

事前準備

今回はGoogleGmailを使う。
Gmailの未読メッセージ一覧の取得を目指してみる。
ただし今回は勉強のためもあるので自前実装したが、GoogleのOAuthはそれ専用のラッパーがありOAuthの処理を自前で書くのは推奨されていないことに注意
事前準備として任意のGoogleアカウントでクライアントIDと秘密鍵を発行する必要がある。
以下を参考にする。
future-architect.github.io

上の手順にも書いてあるがリダイレクトさせるURLは事前に申請する必要がある。
これがないと400番のエラーが返ってくることになるが、私はここでちょっと詰まってしまった。
今回の例でいうと http://127.0.0.1:5000/callback がそれにあたる。

実装

フォルダ構成

├── app.py
├── requirements.txt
└── templates
    ├── index.html
    └── result.html

requirements.txt

Flask
jinja2
requests

ソースコード

index.html

<!DOCTYPE html>
<html lang="ja">
    <head>
        <title>Oauth sample app</title>
        <meta charset="utf-8"/>
    </head>
    <body>
        <h1>{{title}}</h1>
        <p>{{message}}</p>
        <a href="/oauth">
            <button> 
                Connect to Gmail
            </button>
        </a>
    </body>
</html>||<

result.html
>|html|
<!DOCTYPE html>
<html lang="ja">
    <head>
        <title>Oauth sample app</title>
        <meta charset="utf-8"/>
    </head>
    <body>
        {% for message in messages %}
        <li>
            {{message}}
        </li>
        {% endfor %}
    </body>
</html>

app.py

from flask import redirect, render_template, session
from urllib.parse import urlencode
from requests.auth import HTTPBasicAuth
import flask, requests, os, hashlib, base64


app = flask.Flask(__name__)
app.secret_key = os.urandom(32).hex()

class GoogleOAuth():
    client_id: str = "YOUR CLIENT ID"
    client_secret: str = "YOUR CLIENT SECRET"

@app.route("/")
def index():
    session["csrf"] = os.urandom(32).hex()
    return render_template('index.html', title="OAuth sample", message="Connect to Gmail")


@app.route("/oauth")
def oauth():
    if session.get("csrf") is None:
        return redirect("/")
    # Googleの認可サーバーのURL
    authorization_endpoint = "https://accounts.google.com/o/oauth2/v2/auth"
    # 利用したいリソースの権限をスペース区切りで設定する。今回は情報取得のみなのでreadonly属性のみを指定
    scope = "https://www.googleapis.com/auth/gmail.readonly"
    # code_vefierとcode_challengeを設定。S256の仕様に従う。
    code_verifier = os.urandom(43).hex()
    code_challenge = base64.urlsafe_b64encode(hashlib.sha256(code_verifier.encode('utf-8')).digest()).decode('utf-8').replace('=','')
    session["code_verifier"] = code_verifier
    params: dict = {
        "response_type": "code",
        "client_id": GoogleOAuth.client_id,
        "state": session.get("csrf"),
        "scope": scope,
        "redirect_uri": "http://127.0.0.1:5000/callback",
        "code_challenge": code_challenge,
        "code_challenge_method": "S256"
    }
    res = requests.get(authorization_endpoint, params=params)
    return redirect(authorization_endpoint + "?" +  urlencode(params))


@app.route("/callback")
def callback():
    if (session.get("csrf") is None) or (session["csrf"] != state):
        return redirect("/")
    if (session.get("code_verifier") is None):
        return redirect("/")
    # "state"と"code"がクエリパラメータとしてリダイレクトされてくる
    state = flask.request.args.get("state")
    code = flask.request.args.get("code") 
    params = {
        "client_id": GoogleOAuth.client_id,
        "client_secret": GoogleOAuth.client_secret,
        "redirect_uri": "http://127.0.0.1:5000/callback",
        "grant_type": "authorization_code",
        "code": code,
        "code_verifier": session["code_verifier"]
    }
    headers = {"content-type": "application/x-www-form-urlencoded"}
    response = requests.post(
        "https://oauth2.googleapis.com/token", 
        params=params,
        headers=headers,
        auth=HTTPBasicAuth(GoogleOAuth.client_id, GoogleOAuth.client_secret))
    data = response.json()
    access_token = data["access_token"]
    session["access_token"] = access_token
    return redirect("/home")


@app.route("/home")
def home():
    if session.get("access_token") is None:
        return redirect("/")
    access_token = session.get("access_token")
    # GmailのAPI
    resource_endpoint = 'https://gmail.googleapis.com/gmail/v1/users/me/messages'
    params = {'maxResults': 50, 'q': 'is:unread'}
    headers = {'Authorization': 'Bearer {}'.format(access_token)}
    response = requests.get(
        resource_endpoint, 
        params=params,
        headers=headers
    )
    data = response.json()
    # 正常にレスポンスではmessagesというフィールドに値が格納
    messages = data["messages"] if "messages" in data.keys() else [data]
    return render_template("result.html", messages=messages)


if __name__=="__main__":
    app.debug = True
    app.run(host="127.0.0.1", port=5000)

ターミナルで python app.py を実行後、
ブラウザのアドレスバーに 127.0.0.1:5000 と入力する。
こんな感じの結果が返ってくると思う。

・{'id': '185d801397a28c7f', 'threadId': '185d801397a28c7f'}
・{'id': '185d7f65f403db40', 'threadId': '185d7f65f403db40'}
・{'id': '185d75989607f13e', 'threadId': '185d75989607f13e'}
・{'id': '185d6834381417f6', 'threadId': '185d6834381417f6'}
・{'id': '185d26d791a422cf', 'threadId': '185d26d791a422cf'}
・{'id': '185ee6de3d4d53ee', 'threadId': '185ee6de3d4d53ee'}
・{'id': '185ee186a7fddac1', 'threadId': '185ee186a7fddac1'}

本当はメッセージの中身まで取得して表示したかったが、あくまでサンプルアプリなのでここでやめることにした。

*1:Microsoft learnより

カウントダウン数列を考える

カウントダウン数列を考える

京都大学の学園祭の看板で四則演算の組み合わせだけでカウントダウンを表現するというのが話題になっているようだ。 そこで、同じ仕組みで色々な数列に対してどれだけのカウントダウンができるのかを考えてみた。

京都大学の学祭カウントダウンが頭良すぎて感心してしまう→実際できるのか検証してみた

補足として365日前からカウントダウンするにはどうすればいいかを考えてみる。

要件

  1. 与えられた数列に対し順序を固定しその間の四則演算の演算記号を組み替えることで出力を得る
  2. 1から順にどれだけカウントアップできるかを出力する
  3. できれば出力に使われた四則演算の組み合わせを見れるようにしたい

実装

動作環境 python3.10.6

from typing import List
import itertools

class Outputs:
    def __init__(self):
        self.equations: List[str] = []
        self.outputs: List[float] = []

    def add(self, equation, output):
        self.equations.append(equation)
        self.outputs.append(output)
        
    def find(self, num) -> str:
        try:
            index = self.outputs.index(num)
            return self.equations[index]
        except:
            raise Exception()
        
def count_down_nums(nums: List[int], dry_run: bool = True) -> int:
    check_outputs: List[int] = list(range(1,9999))
    operators: List[str] = ['+','-','*','/']
    outputs: Outputs = Outputs()
    operator_combinations = list(itertools.product(operators, repeat=len(nums)-1))
    for operator_combination in operator_combinations:
        equation: str = ''
        for i,num in enumerate(nums):
            if i==0:
                equation += str(num)
            else:
                equation += operator_combination[i-1]
                equation += str(num)
        outputs.add(equation, eval(equation))
    for i,check_output in enumerate(check_outputs):
        try:
            equation = outputs.find(check_output)
            if dry_run == False:
                print(equation +  '=' + str(check_output))
            continue
        except:
            if i==0:
                return 0
            return check_outputs[i-1]
    return -1 # means "more than 9999"

ケース1

まずは元ネタと同じ数列 [6,4,5,2,1] でやってみる

count_down_nums([6,4,5,2,1], dry_run=False)
6+4-5*2+1=1
6+4-5-2-1=2
6+4-5-2*1=3
6+4-5-2+1=4
6-4+5-2*1=5
6+4-5+2-1=6
6+4-5+2*1=7
6+4-5+2+1=8
6-4+5+2*1=9
6-4+5+2+1=10
6-4+5*2-1=11
6+4+5-2-1=12
6+4+5-2*1=13
6+4+5-2+1=14
6+4*5/2-1=15
6+4+5+2-1=16
6+4+5+2*1=17
6+4+5+2+1=18
6+4+5*2-1=19
6+4+5*2*1=20
6+4+5*2+1=21
6*4-5+2+1=22
6+4*5-2-1=23
6+4*5-2*1=24
6+4*5-2+1=25
6*4+5-2-1=26
6+4*5+2-1=27
6+4*5+2*1=28
6+4*5+2+1=29
6*4+5+2-1=30
6*4+5+2*1=31
6*4+5+2+1=32
6*4+5*2-1=33
6*4+5*2*1=34
6*4+5*2+1=35





35

この仕組だと35日前からカウントダウンできるらしい

ケース2

京都大学の学祭カウントダウンが頭良すぎて感心してしまう→実際できるのか検証してみた

要らないと言われた「3」の立場、気持ちを考えると胸が潰れそう…

2022/10/24 09:24

どうして3がないのだと言われていたので、順当に6からの降順 [6,5,4,3,2,1] でやってみる

count_down_nums([6,5,4,3,2,1], dry_run=False)
6+5-4-3-2-1=1
6+5-4-3-2*1=2
6+5-4-3-2+1=3
6+5-4*3/2-1=4
6+5-4-3+2-1=5
6+5-4-3+2*1=6
6+5-4+3-2-1=7
6+5+4-3*2-1=8
6+5+4-3-2-1=9
6+5+4-3-2*1=10
6+5+4-3-2+1=11
6+5-4+3+2*1=12
6+5+4-3+2-1=13
6+5+4-3+2*1=14
6+5+4+3-2-1=15
6+5+4+3-2*1=16
6+5+4+3-2+1=17
6+5+4*3/2+1=18
6+5+4+3+2-1=19
6+5+4+3+2*1=20
6+5+4+3+2+1=21
6+5+4+3*2+1=22
6*5-4*3/2-1=23
6+5+4*3+2-1=24
6+5+4*3+2*1=25
6+5+4*3+2+1=26
6+5*4+3-2*1=27
6+5*4+3-2+1=28
6*5+4-3-2*1=29
6+5*4+3+2-1=30
6+5*4+3+2*1=31
6+5*4+3+2+1=32
6+5*4+3*2+1=33
6+5+4*3*2-1=34
6+5+4*3*2*1=35
6+5+4*3*2+1=36
6+5*4*3/2+1=37
6*5+4+3+2-1=38
6*5+4+3+2*1=39
6*5+4+3+2+1=40
6*5+4+3*2+1=41
6*5*4/3+2*1=42
6*5+4*3+2-1=43
6*5+4*3+2*1=44
6*5+4*3+2+1=45
6*5/4*3*2+1=46





46

こっちのほうが表現力豊かじゃないか。 どうして京都大学の学祭委員の人たちは[6,5,4,3,2,1]にせずにあえて[6,4,5,2,1]なんて不規則な数列にしたのか気になる。(第64回だからか…)

ケース3

次は [7,6,5,4,3,2,1] でやってみる

count_down_nums([7,6,5,4,3,2,1])
64

ケース4

このまま増えていくとどこまでカウントダウンできるのか気になったので

  • [6,5,4,3,2,1]
  • [7,6,5,4,3,2,1]

...

とやってみる

print("6~1" ,count_down_nums(range(1,7)[::-1]))
print("7~1" ,count_down_nums(range(1,8)[::-1]))
print("8~1" ,count_down_nums(range(1,9)[::-1]))
print("9~1" ,count_down_nums(range(1,10)[::-1]))
print("10~1" ,count_down_nums(range(1,11)[::-1]))
print("11~1" ,count_down_nums(range(1,12)[::-1])) 
6~1 46
7~1 64
8~1 102
9~1 152
10~1 248
11~1 1133

ケース4

等比数列ならもっと表現力あるんじゃないか?

print("2^5~1" ,count_down_nums([32,16,8,4,2,1]))
print("2^6~1" ,count_down_nums([64,32,16,8,4,2,1]))
print("2^7~1" ,count_down_nums([128,64,32,16,8,4,2,1]))
print("2^8~1" ,count_down_nums([256,128,64,32,16,8,4,2,1]))
print("2^9~1" ,count_down_nums([512,256,128,64,32,16,8,4,2,1]))
2^5~1 59
2^6~1 139
2^7~1 283
2^8~1 595
2^9~1 1163

結論

365日をカウントダウンするなら

  • [11,10,9,9,7,6,5,4,3,2,1]
  • [256,128,64,32,16,8,4,2,1]

でできる