<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Stan on suzuna's memo</title><link>https://suzunano.net/tags/stan/</link><description>Recent content in Stan on suzuna's memo</description><generator>Hugo -- gohugo.io</generator><language>ja-JP</language><lastBuildDate>Tue, 09 Dec 2025 00:00:00 +0900</lastBuildDate><atom:link href="https://suzunano.net/tags/stan/index.xml" rel="self" type="application/rss+xml"/><item><title>ConoHaオブジェクトストレージをboto3で使ってみる</title><link>https://suzunano.net/posts/conoha-object-storage-with-boto3/</link><pubDate>Thu, 11 Dec 2025 00:00:00 +0900</pubDate><guid>https://suzunano.net/posts/conoha-object-storage-with-boto3/</guid><description>&lt;p&gt;この記事は&lt;a href="https://qiita.com/advent-calendar/2025/conoha" target="_blank" rel="noopener noreferrer"&gt;ConoHa Advent Calendar
2025&lt;/a&gt;の11日目の記事です。このはのアドカレは3年ぶりの参加です。&lt;/p&gt;
&lt;p&gt;今年はあんずちゃんがいっぱいツイートしてくれて嬉しいです。&lt;/p&gt;
&lt;p&gt;さて、2025/8/28に、ConoHaのオブジェクトストレージは&lt;a href="https://vps.conoha.jp/news/?ap=2015054049" target="_blank" rel="noopener noreferrer"&gt;S3互換APIを提供開始&lt;/a&gt;しました。&lt;/p&gt;
&lt;p&gt;S3互換APIがあるということは、オブジェクトストレージの代表格であるAmazon
S3を操作するAWS CLIや、AWSのPython
SDKであるboto3が使えてうれしいところです。&lt;/p&gt;
&lt;p&gt;この記事では、AWSリソースを操作するPythonライブラリのboto3を使ってConoHaのオブジェクトストレージを操作してみます。S3の経験はあるけどConoHaオブジェクトストレージは使ったことがない人の参考になれば幸いです。&lt;/p&gt;
&lt;h2 id="conohaオブジェクトストレージとは"&gt;ConoHaオブジェクトストレージとは&lt;/h2&gt;
&lt;p&gt;名前のとおり、ConoHaが提供するオブジェクトストレージです。&lt;/p&gt;
&lt;p&gt;実際に使用した容量にかかわらず、確保した容量に対して100GBあたり545円/月の料金がかかります。その一方、1時間単位で使った時間分だけ課金されるので、試しやすくていいですね。また、S3やGoogle
Cloud
Storageなどと異なり、アウトバウンドの通信に転送料金がかかりません。&lt;/p&gt;
&lt;p&gt;ConoHaオブジェクトストレージはオープンソースのオブジェクトストレージであるOpenStack
Swiftで構築されています。そのため、従来はOpenStack
SwiftのAPIを叩くか、OpenStack
SwiftからS3に「翻訳」してくれるS3Proxyをローカルなどに立てたうえでS3相当の操作をする必要があり、S3と同様に扱うには若干の壁があるのが正直なところでした。&lt;/p&gt;
&lt;p&gt;S3互換APIがあれば、S3との差異をあまり意識することなく操作できて扱いやすいですね。&lt;/p&gt;
&lt;h2 id="事前準備"&gt;事前準備&lt;/h2&gt;
&lt;p&gt;まずはオブジェクトストレージを使う前に準備をしておきましょう。手順は以下の通りです。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;オブジェクトストレージの契約&lt;/li&gt;
&lt;li&gt;APIユーザーの作成&lt;/li&gt;
&lt;li&gt;EC2 Credentialの発行&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="オブジェクトストレージの契約"&gt;オブジェクトストレージの契約&lt;/h3&gt;
&lt;p&gt;事前にConoHaのコントロールパネルにログインし、左のパネルから「オブジェクトストレージ」を選び、適当に容量を選択します。お試しならまず100GBでいいでしょう。&lt;a href="https://support.conoha.jp/v/addobjectstorage" target="_blank" rel="noopener noreferrer"&gt;公式マニュアル&lt;/a&gt;に画像付きで分かりやすくまとまっているのでこれを参考にしてください。&lt;/p&gt;
&lt;p&gt;mendoitarou_さんの昨年のアドカレの記事も分かりやすいです！（&lt;a href="https://qiita.com/mendoitarou_/items/d027a8a36980e2b286eb" target="_blank" rel="noopener noreferrer"&gt;ConoHaのオブジェクトストレージを使ってみる
#Conoha -
Qiita&lt;/a&gt;）&lt;/p&gt;
&lt;h3 id="apiユーザーの作成"&gt;APIユーザーの作成&lt;/h3&gt;
&lt;p&gt;次に、APIユーザーを作成したことがない場合は、&lt;a href="https://doc.conoha.jp/reference/api-vps3/api-cp-vps3/cp-create_api_user-v3" target="_blank" rel="noopener noreferrer"&gt;公式マニュアル&lt;/a&gt;に従って、コントロールパネルの左のパネルの「API」からAPIユーザーを作成しておいてください。&lt;/p&gt;
&lt;h3 id="ec2-credentialの発行"&gt;EC2 Credentialの発行&lt;/h3&gt;
&lt;p&gt;こちらも既にやったことがあれば飛ばしてOKです。&lt;/p&gt;
&lt;p&gt;EC2
Credentialは、ConoHaのオブジェクトストレージをS3互換APIから操作するのに必要なクレデンシャルです&lt;sup id="fnref:1"&gt;&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref"&gt;1&lt;/a&gt;&lt;/sup&gt;。AWSの&lt;code&gt;aws_access_key_id&lt;/code&gt;と&lt;code&gt;aws_secret_access_key&lt;/code&gt;に相当します。&lt;/p&gt;
&lt;p&gt;まず、コントロールパネルの「API」を開きます。&lt;/p&gt;
&lt;img src="conoha-control-panel.jpg" width=80%&gt;
&lt;p&gt;ちょうど端っこからこのはちゃんが見えてかわいいですね。&lt;/p&gt;
&lt;p&gt;以下をそれぞれメモしておきます。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;APIユーザーの「ユーザーID」&lt;/li&gt;
&lt;li&gt;APIユーザーの「パスワード」&lt;/li&gt;
&lt;li&gt;テナント情報の「テナントID」&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;EC2
Credentialを発行するためにはトークンが必要なので、先にトークンを発行します（&lt;a href="https://doc.conoha.jp/reference/api-vps3/api-identity-vps3/identity-post_tokens-v3/" target="_blank" rel="noopener noreferrer"&gt;公式マニュアル&lt;/a&gt;）。&lt;/p&gt;
&lt;p&gt;次に、EC2
Credentialを発行します（&lt;a href="https://doc.conoha.jp/reference/api-vps3/api-identity-vps3/identity-create_credential-v3" target="_blank" rel="noopener noreferrer"&gt;公式マニュアル&lt;/a&gt;）。&lt;/p&gt;
&lt;p&gt;合わせて、こんなコードで発行できます。このコードを含め、本記事の全てのコードはPython=3.13.7,
boto3=1.42.5, botocore=1.42.5で実行しました。&lt;/p&gt;
&lt;details class="code-fold"&gt;
&lt;summary&gt;クリックで折り畳みが開きます&lt;/summary&gt;
&lt;pre&gt;&lt;code class="language-python"&gt;import os
import requests
# os.environの各Keyの環境変数に設定していることを前提とします
# コード中べた書きでもいいですが、セキュリティ上推奨しません
# それぞれ順に
# - APIユーザーの「ユーザーID」
# - APIユーザーの「パスワード」
# - テナント情報の「テナントID」
api_user_id = os.environ[&amp;quot;CONOHA_API_USER_ID&amp;quot;]
api_user_password = os.environ[&amp;quot;CONOHA_API_USER_PASSWORD&amp;quot;]
tenant_id = os.environ[&amp;quot;CONOHA_TENANT_ID&amp;quot;]
# トークンの発行
auth_url = &amp;quot;https://identity.c3j1.conoha.io/v3/auth/tokens&amp;quot;
auth_data = {
&amp;quot;auth&amp;quot;: {
&amp;quot;identity&amp;quot;: {
&amp;quot;methods&amp;quot;: [&amp;quot;password&amp;quot;],
&amp;quot;password&amp;quot;: {
&amp;quot;user&amp;quot;: {
&amp;quot;id&amp;quot;: api_user_id,
&amp;quot;password&amp;quot;: api_user_password
}
}
},
&amp;quot;scope&amp;quot;: {
&amp;quot;project&amp;quot;: {
&amp;quot;id&amp;quot;: tenant_id
}
}
}
}
resp = requests.post(auth_url, json=auth_data)
token = resp.headers[&amp;quot;x-subject-token&amp;quot;]
# EC2 Credentialの作成
credential_url = f&amp;quot;https://identity.c3j1.conoha.io/v3/users/{api_user_id}/credentials/OS-EC2&amp;quot;
credential_data = {&amp;quot;tenant_id&amp;quot;: tenant_id}
resp = requests.post(
credential_url,
json=credential_data,
headers={&amp;quot;X-Auth-Token&amp;quot;: token}
)
credential = resp.json()[&amp;quot;credential&amp;quot;]
access_key = credential[&amp;quot;access&amp;quot;]
secret_key = credential[&amp;quot;secret&amp;quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;/details&gt;
&lt;p&gt;&lt;code&gt;api_user_id&lt;/code&gt;, &lt;code&gt;api_user_password&lt;/code&gt;, &lt;code&gt;tenant_id&lt;/code&gt;, &lt;code&gt;access_key&lt;/code&gt;,
&lt;code&gt;secret_key&lt;/code&gt;は他人に知られないよう管理に注意してください（&lt;code&gt;token&lt;/code&gt;は有効期限が24時間ではありますが、これも漏らさない方がもちろんよいですね）。&lt;/p&gt;
&lt;p&gt;access_keyとsecret_keyの中身をそれぞれメモしておいてください。EC2
Credentialは1ユーザーにつき3つしか作成できません。もし忘れた場合は、同様にAPIで既存のEC2
Credentialを削除してから再度作成してください。&lt;/p&gt;
&lt;h2 id="boto3を使ってconohaオブジェクトストレージで遊んでみる"&gt;boto3を使ってConoHaオブジェクトストレージで遊んでみる&lt;/h2&gt;
&lt;p&gt;参考: &lt;a href="https://doc.conoha.jp/reference/api-vps3/api-objectstorage-vps3" target="_blank" rel="noopener noreferrer"&gt;Object Storage
API｜ConoHaドキュメントサイト&lt;/a&gt;&lt;/p&gt;
&lt;h3 id="clientインスタンスの作成"&gt;clientインスタンスの作成&lt;/h3&gt;
&lt;p&gt;まずは&lt;code&gt;boto3.client&lt;/code&gt;を作ります。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-python"&gt;import boto3
from botocore.config import Config
cli = boto3.client(
&amp;quot;s3&amp;quot;,
endpoint_url=endpoint_url,
aws_access_key_id=access_key,
aws_secret_access_key=secret_key,
config=Config(
signature_version=&amp;quot;s3v4&amp;quot;,
request_checksum_calculation=&amp;quot;when_required&amp;quot;,
response_checksum_validation=&amp;quot;when_required&amp;quot;,
),
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;endpoint_url&lt;/code&gt;は、先ほどのコントロールパネルの画像の「エンドポイント」の「S3
Service」のURLです。&lt;/p&gt;
&lt;p&gt;ポイントは、&lt;code&gt;request_checksum_calculation=&amp;quot;when_required&amp;quot;&lt;/code&gt;と、
&lt;code&gt;response_checksum_validation=&amp;quot;when_required&amp;quot;&lt;/code&gt;を指定することです。&lt;code&gt;&amp;quot;when_required&amp;quot;&lt;/code&gt;にしておかないと、オブジェクトをアップロードするときに&lt;code&gt;XAmzContentSHA256Mismatch&lt;/code&gt;などというエラーが出ます。&lt;/p&gt;
&lt;p&gt;&lt;a href="https://aws.amazon.com/jp/blogs/news/introducing-default-data-integrity-protections-for-new-objects-in-amazon-s3/" target="_blank" rel="noopener noreferrer"&gt;S3が提供する新しいチェックサム機能&lt;/a&gt;に合わせて、boto3&amp;gt;=1.36.0では、これらの引数はデフォルトで&lt;code&gt;&amp;quot;when_supported&amp;quot;&lt;/code&gt;になりました。&lt;a href="https://github.com/boto/boto3/issues/4392" target="_blank" rel="noopener noreferrer"&gt;boto3のissue&lt;/a&gt;では、S3互換ストレージでは対応していないものもあるため、エラーが出たら&lt;code&gt;&amp;quot;when_required&amp;quot;&lt;/code&gt;にするようにとありますので、そのとおりやってあげれば回避できます。&lt;/p&gt;
&lt;p&gt;これは地味にはまりポイントでした。&lt;/p&gt;
&lt;h3 id="コンテナの作成"&gt;コンテナの作成&lt;/h3&gt;
&lt;p&gt;S3をboto3で操作したことがあれば、ここからは全く同じ操作で扱うことができます。&lt;/p&gt;
&lt;p&gt;コンテナとはS3でいうバケットに相当するものです。&lt;code&gt;conoha&lt;/code&gt;という名前のコンテナを作ってみます。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-python"&gt;cli.create_bucket(Bucket=&amp;quot;conoha&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id="コンテナのリストアップ"&gt;コンテナのリストアップ&lt;/h3&gt;
&lt;p&gt;作ったコンテナをリストアップします。AWS
CLIでいう、&lt;code&gt;aws s3 ls&lt;/code&gt;相当の操作です。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-python"&gt;response = cli.list_buckets()
for bucket in response.get(&amp;quot;Buckets&amp;quot;, []):
print(bucket[&amp;quot;Name&amp;quot;])
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;conoha
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;今作った&lt;code&gt;conoha&lt;/code&gt;がありますね！&lt;/p&gt;
&lt;h3 id="オブジェクトのアップロード"&gt;オブジェクトのアップロード&lt;/h3&gt;
&lt;p&gt;2025年12月に公開されたばかりの、このはちゃんの清楚かわいい冬壁紙をアップロードしてみます。&lt;/p&gt;
&lt;img src="conoha-wallpaper-2025winter-1280x800.jpg" width=80%&gt;
&lt;p&gt;壁紙は&lt;a href="https://conoha.mikumo.com/wallpaper/" target="_blank" rel="noopener noreferrer"&gt;公式サイト&lt;/a&gt;からダウンロードできます。眺めていると幸せな気持ちになれますね。皆さんもダウンロードして自分のデバイスの壁紙に設定しましょう。昨今の公式素材といえばSNSアイコンが多い印象ですが、壁紙を配布してくれていて大変ありがたい…&lt;/p&gt;
&lt;p&gt;わたしが好きなのはこのおまつりこのはちゃんですね。これはきっと夏の終わりに見た幻…&lt;/p&gt;
&lt;img src="conoha-wallpaper-omatsuri-1280x800.jpg" width=80%&gt;
&lt;p&gt;さて、さっきの冬壁紙（&lt;code&gt;conoha-wallpaper-2025winter-1280x800.jpg&lt;/code&gt;）を、コンテナ&lt;code&gt;conoha&lt;/code&gt;に、&lt;code&gt;mikumo/conoha.jpg&lt;/code&gt;というkeyでアップロードしてみます。S3でいうところの&lt;code&gt;s3://conoha/mikumo/conoha.jpg&lt;/code&gt;にアップロードするということです。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-python"&gt;cli.upload_file(&amp;quot;conoha-wallpaper-2025winter-1280x800.jpg&amp;quot;, &amp;quot;conoha&amp;quot;, &amp;quot;mikumo/conoha.jpg&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;オブジェクトストレージなのでディレクトリという概念はありませんが、keyをスラッシュで区切ることで仮想的にディレクトリのような階層構造で扱うことができます。&lt;/p&gt;
&lt;h3 id="オブジェクトのリストアップ"&gt;オブジェクトのリストアップ&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;conoha&lt;/code&gt;というコンテナにある全てのオブジェクトをリストアップします。&lt;code&gt;aws s3 ls s3://conoha&lt;/code&gt;相当の操作ですね。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-python"&gt;# 以下はオブジェクトが1000件を超える場合ページネーションが必要
# resp = cli.list_objects_v2(Bucket=&amp;quot;conoha&amp;quot;)
# for obj in resp.get(&amp;quot;Contents&amp;quot;, []):
# print(obj[&amp;quot;Key&amp;quot;])
# こちらはページネーションに対応
paginator = cli.get_paginator(&amp;quot;list_objects_v2&amp;quot;)
for page in paginator.paginate(Bucket=&amp;quot;conoha&amp;quot;):
for obj in page.get(&amp;quot;Contents&amp;quot;, []):
print(obj[&amp;quot;Key&amp;quot;])
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;mikumo/conoha.jpg
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;今アップロードした&lt;code&gt;mikumo/conoha.jpg&lt;/code&gt;がありますね！&lt;/p&gt;
&lt;h3 id="オブジェクトのダウンロード"&gt;オブジェクトのダウンロード&lt;/h3&gt;
&lt;p&gt;ローカルにダウンロードもできます。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-python"&gt;cli.download_file(&amp;quot;conoha&amp;quot;, &amp;quot;mikumo/conoha.jpg&amp;quot;, &amp;quot;mikumo-conoha.jpg&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id="presigned-urlの発行"&gt;Presigned URLの発行&lt;/h3&gt;
&lt;p&gt;非公開のオブジェクトでも、S3のPresigned
URLという、有効期限付きのダウンロードリンクを発行することができます。&lt;/p&gt;
&lt;p&gt;以下の例では、&lt;code&gt;mikumo/conoha.jpg&lt;/code&gt;に対して、1時間だけ有効なURLを発行します。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-python"&gt;url = cli.generate_presigned_url(
&amp;quot;get_object&amp;quot;,
Params={&amp;quot;Bucket&amp;quot;: &amp;quot;conoha&amp;quot;, &amp;quot;Key&amp;quot;: &amp;quot;mikumo/conoha.jpg&amp;quot;},
ExpiresIn=3600 # 3600 secs = 1 hour
)
print(url)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;例えばブラウザからこのURLにアクセスすると、画像を開くことができます。&lt;/p&gt;
&lt;p&gt;ConoHaオブジェクトストレージは&lt;a href="https://doc.conoha.jp/reference/api-vps3/api-objectstorage-vps3/object-temporary_url-v3" target="_blank" rel="noopener noreferrer"&gt;一時的Web公開&lt;/a&gt;で同様の操作ができます。これは、OpenStack
Swiftが提供するSwift
TempURLというOpenStack独自の一時的なアクセスURLを発行する仕組みによります。&lt;/p&gt;
&lt;p&gt;一方でS3互換APIがありますので、ConoHa公式には記載がありませんがS3相当のPresigned
URLも発行できます（上のコードです）。以下の記事にあるようにS3互換のCloudflare
R2でもできるそうなので、ConoHaでもできるかな？と思ったらできました。&lt;/p&gt;
&lt;p&gt;&lt;a href="https://dev.classmethod.jp/articles/cloudflare-r2-presigned-url/" target="_blank" rel="noopener noreferrer"&gt;S3互換のCloudflare R2で署名付きURLを発行する（AWS CLI, Python + Boto3）
|
DevelopersIO&lt;/a&gt;&lt;/p&gt;
&lt;h3 id="オブジェクトの削除"&gt;オブジェクトの削除&lt;/h3&gt;
&lt;p&gt;オブジェクトストレージは現在使っている容量以下に契約容量を下げることはできませんし、オブジェクトとコンテナを全て削除しないと解約できません（&lt;a href="https://support.conoha.jp/v/addobjectstorage/" target="_blank" rel="noopener noreferrer"&gt;公式マニュアル&lt;/a&gt;）。そのため、オブジェクトとコンテナの削除の方法もみておきましょう。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-python"&gt;cli.delete_object(Bucket=&amp;quot;conoha&amp;quot;, Key=&amp;quot;mikumo/conoha.jpg&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;ばいばいこのはちゃん…&lt;/p&gt;
&lt;h3 id="コンテナの削除"&gt;コンテナの削除&lt;/h3&gt;
&lt;p&gt;最後にコンテナを削除します。なお、コンテナの中身が全て空でなければ削除できません（エラーが返ります）。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-python"&gt;cli.delete_bucket(Bucket=&amp;quot;conoha&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;これで全て削除されたのでオブジェクトストレージを解約しても構いませんが、契約し続けたほうがこのはちゃんが喜びます。&lt;/p&gt;
&lt;h2 id="おわりに"&gt;おわりに&lt;/h2&gt;
&lt;p&gt;S3の経験があれば簡単に触れますし、boto3のドキュメントはいっぱいあるのでS3の経験がなくても触れそうです。&lt;/p&gt;
&lt;p&gt;活用例を調べてみると、2015年のアドカレ記事でConoHaのインスタンスのリバースプロキシと合わせた画像アップローダーサイトが面白いなと思いました。&lt;/p&gt;
&lt;p&gt;&lt;a href="https://qiita.com/AKB428/items/812d6441b06a00d4819e" target="_blank" rel="noopener noreferrer"&gt;【月額1350円でできる！！】ConoHaのオブジェクトストレージを使ってWEB画像アップローダーサイトを作ってみよう
#Ruby - Qiita&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;転送料金フリーなメリットを活かしてなんか作れたらいいな！&lt;/p&gt;
&lt;p&gt;個人的な話ですが、3年前のこのはのアドカレはわたしがはじめて書いたアドカレなので、このはのアドカレには少し思い入れがあります（そのときの記事:
&lt;a href="../conoha-chatbot"&gt;GPT-2で作ったConoHa上のこのはちゃんbotとSlackで会話する&lt;/a&gt;）。久々に参加できて楽しかったので、来年も何か記事を出せるようにConoHaを触りながら考えてみます！&lt;/p&gt;
&lt;p&gt;（本文中で引用した壁紙は©GMO Internet, Inc., 再使用禁止です）&lt;/p&gt;
&lt;div class="footnotes" role="doc-endnotes"&gt;
&lt;hr&gt;
&lt;ol&gt;
&lt;li id="fn:1"&gt;
&lt;p&gt;正確には、OpenStackをS3互換APIで使うのに必要なクレデンシャルです。&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</description></item><item><title>多変量確率的ボラティリティモデルで相関係数の時変性をとらえる</title><link>https://suzunano.net/posts/multivariate-stochastic-volatility-model/</link><pubDate>Tue, 09 Dec 2025 00:00:00 +0900</pubDate><guid>https://suzunano.net/posts/multivariate-stochastic-volatility-model/</guid><description>&lt;h2 id="はじめに"&gt;はじめに&lt;/h2&gt;
&lt;p&gt;この記事は&lt;a href="https://qiita.com/advent-calendar/2025/market-api" target="_blank" rel="noopener noreferrer"&gt;マケデコ Advent Calendar
2025&lt;/a&gt;の9日目の記事です。&lt;/p&gt;
&lt;p&gt;資産間のリターンの相関係数を求める方法としては、過去一定期間のリターンからローリング相関を計算するものが広く用いられています。しかし、この方法には、ローリングのウィンドウの期間の長さによって値が変わったり、市場の急変に追随するのが遅かったりするという課題が存在します。&lt;/p&gt;
&lt;p&gt;これを解決する方法として、ボラティリティと相関を同時にモデリングする、多変量の確率的ボラティリティモデル（Multivariate
Stochastic Volatility (MSV) モデル）を紹介します。&lt;/p&gt;
&lt;p&gt;MSVモデルをPythonとStanで実装し、2008年～2025年のTOPIXと東証REIT指数の相関係数を推定してみました。相関係数はおおむね0.4程度ですが、2016年～2020年や2025年は0.2程度まで低下していました。&lt;/p&gt;
&lt;p&gt;金融工学の論文実装でアドカレを書いている人があんまりいないのでこういうのもありでしょう。マケデコのアドカレは気付けば3年連続で書いています。よかったら過去記事も読んでみてください。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;2023年: &lt;a href="../stochastic-volatility-model"&gt;TOPIXのボラティリティをStochastic Volatilityモデル + R +
Stanで推定する&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;2024年: &lt;a href="../realized-volatility/"&gt;Realized Volatilityの理論と実装&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;毎年ボラティリティネタで書いてますね。これはわたしの趣味です。&lt;/p&gt;
&lt;p&gt;ちなみにこの記事は、&lt;a href="../stochastic-volatility-model"&gt;2023年の記事&lt;/a&gt;のStochastic
Volatilityモデルを単一資産から複数資産に拡張したものですので、この過去の記事も参考になると思います。&lt;/p&gt;
&lt;h2 id="相関係数のモデリングの重要性"&gt;相関係数のモデリングの重要性&lt;/h2&gt;
&lt;p&gt;リターンの相関係数はアセットアロケーションなどに使われるため、個々の資産のボラティリティとならんで重要なのはいうまでもありません。ではどうやって相関係数を求めればよいのでしょうか？&lt;/p&gt;
&lt;p&gt;よく知られているのは、各資産の過去n日のリターンどうしの相関係数を求め、1日ずつスライドさせていく、いわゆるローリング相関係数です。&lt;/p&gt;
&lt;p&gt;2008/5〜2025/12のTOPIXと東証REIT指数（両方配当なし）のデータを用いて、それぞれの対数リターンの過去n営業日ローリング相関係数をn=250でプロットしてみると以下のようになります。これは過去1年のローリングに相当します。なお、TOPIXと東証REIT指数のデータはJ-Quantsから取りました。&lt;/p&gt;
&lt;p&gt;&lt;img src="index_files/figure-commonmark/cell-3-output-1.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;これは広く用いられている方法ですが、問題点があります。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;nの値によって相関係数の値が大きく変わる&lt;/li&gt;
&lt;li&gt;ウィンドウ期間中は相関係数が一定という前提のもとに成り立つ&lt;/li&gt;
&lt;li&gt;極端なリターンがあると相関係数が過度に変化する&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;特に2点目と3点目ですが、ローリング相関は、ウィンドウ期間中の相関係数が一定であることを暗黙の前提とします。その場合、ウィンドウ期間中の標本相関係数は真の相関の推定値として解釈できます。&lt;/p&gt;
&lt;p&gt;しかし、市場は急に変動することがあるためその仮定は現実的ではありません。極端なリターンがあると相関係数が急変動する一方、相関係数の構造変化には追随が遅れてしまいます。極端なリターンの例としては、2011年3月の東日本大震災のときの市場の急変動により、プロットのとおり相関係数が急に上がっていることが挙げられます。日次リターンではなく月次リターンなどより長い期間のリターンから求めることで極端なリターンへの対応ができますが、構造変化への追随はより遅れます。&lt;/p&gt;
&lt;p&gt;これらの問題に対する一つの解決策は、それぞれの資産の日次のリターンから、各資産のボラティリティとその相関係数をモデリングすることです。&lt;/p&gt;
&lt;p&gt;様々な方法が提案されていますが&lt;sup id="fnref:1"&gt;&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref"&gt;1&lt;/a&gt;&lt;/sup&gt;、その一つがMSVモデルです。これは、観測されるリターンから、潜在変数である各資産のボラティリティと資産間の相関係数を同時にモデリングするものです。&lt;/p&gt;
&lt;p&gt;複雑な状態空間モデルですが、Stanなどで実装すればボラティリティや相関係数の推定値を得ることができます。またモデルの拡張も容易です。&lt;/p&gt;
&lt;p&gt;MSVモデルをアセットアロケーションに適用した研究としては、Aguilar and
West (2000) や、それを発展させてスパース性を持たせたZhou, et al. (2014)
が有名です。特に前者では、MSVモデルを用いたアセットアロケーションは、1992年のイギリスのERM脱退時のポンド相場のような構造変化時によいパフォーマンスを示すことが報告されています。&lt;/p&gt;
&lt;h2 id="モデル"&gt;モデル&lt;/h2&gt;
&lt;p&gt;いま、資産1と資産2の$t$日における日次終値をそれぞれ$S_{t,1}, S_{t,2}$とします。&lt;/p&gt;
&lt;p&gt;このとき、$t$日におけるリターンを、対数リターンの100倍として、それぞれ$r_{t,1} = 100 (\log(S_{t,1}) - \log(S_{t-1,1})), r_{t,2} = 100 (\log(S_{t,2}) - \log(S_{t-1,2}))$とします。100倍するのは、パーセント表記にするということです。&lt;/p&gt;
&lt;p&gt;以下のように定式化したMSVモデルを考えます。&lt;/p&gt;
&lt;p&gt;$$
\begin{aligned}
y_{t,i} &amp;amp;= \exp(h_{t,i}/2) \epsilon_{t,i}, \quad i = 1, 2 \\
(\epsilon_{t,1}, \epsilon_{t,2})&amp;rsquo; &amp;amp;\sim N\left(\begin{pmatrix} 0 \\ 0 \end{pmatrix}, \begin{pmatrix} 1 &amp;amp; \rho_t \\ \rho_t &amp;amp; 1 \end{pmatrix}\right) \\
h_{t+1,i} &amp;amp;= \mu_i + \phi_i (h_{t,i} - \mu_i) + \eta_{t,i}, \quad \eta_{t,i} \sim N(0, \sigma_{\eta,i}^2) \\
g_{t+1} &amp;amp;= g_t + \zeta_t, \quad \zeta_t \sim N(0, \sigma_\zeta^2) \\
\rho_t &amp;amp;= \tanh(g_t)
\end{aligned}
$$&lt;/p&gt;
&lt;p&gt;ただし、$y_{t,i} = r_{t, i}$とします。これは日次リターンが有意に正でも負でもないということです。多くの実証研究ではそれが示されていますが、仮にそうではない場合は、第1式に定数項か、その定数を時変にしてその状態方程式を加えます。&lt;/p&gt;
&lt;p&gt;このとき、資産$i (i = 1, 2)$の$t$日におけるボラティリティは$\sigma_{t,i} = \exp(h_{t,i}/2)$、相関係数は$\rho_t$となります。&lt;/p&gt;
&lt;p&gt;分散の対数がAR(1)過程に従い、相関係数はランダムウォークする$g_t$を$\tanh({g_{t}})$で相関係数が取るべき-1から1までの間に押し込めたものとして同時にモデリングしています。&lt;/p&gt;
&lt;p&gt;なお、MSVモデルにはさまざまなバリエーションが存在します&lt;sup id="fnref:2"&gt;&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref"&gt;2&lt;/a&gt;&lt;/sup&gt;。多変量の観測系列があり、各系列のボラティリティが確率的で時変する潜在変数であればMSVモデルといえます。例えば相関係数をつくる$g$は上の式ではなく平均回帰性を持つようにAR(1)過程にするなど、細かい差異がいろいろ存在します。大森
(2019) に詳しく書かれています。&lt;/p&gt;
&lt;h2 id="実装"&gt;実装&lt;/h2&gt;
&lt;p&gt;上述のモデルをStanで書いて、Pythonからキックすることでパラメータを推定します。この実装のセクションはStanの知識が必要ですので、Stanに慣れていない方はこの章は読み飛ばしても大丈夫ですが、自分でやってみたい場合は参考にしてください。最近はStan以外にもベイズ推定のフレームワークがいろいろあるので好きなものを使ってください。&lt;/p&gt;
&lt;p&gt;いま、以下のようにTOPIXと東証REIT指数の終値と対前日対数リターン（100倍したもの）をpolars.DataFrameで持っているとします。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;shape: (4_303, 5)
┌────────────┬────────────┬───────────┬───────────┬───────────┐
│ Date ┆ CloseTopix ┆ RetTopix ┆ CloseReit ┆ RetReit │
│ --- ┆ --- ┆ --- ┆ --- ┆ --- │
│ date ┆ f64 ┆ f64 ┆ f64 ┆ f64 │
╞════════════╪════════════╪═══════════╪═══════════╪═══════════╡
│ 2008-05-08 ┆ 1372.95 ┆ -1.469897 ┆ 1585.12 ┆ 0.608743 │
│ 2008-05-09 ┆ 1341.76 ┆ -2.297952 ┆ 1550.03 ┆ -2.238583 │
│ 2008-05-12 ┆ 1342.79 ┆ 0.076735 ┆ 1535.05 ┆ -0.971133 │
│ 2008-05-13 ┆ 1360.05 ┆ 1.277192 ┆ 1547.59 ┆ 0.813593 │
│ 2008-05-14 ┆ 1373.04 ┆ 0.95058 ┆ 1556.18 ┆ 0.553522 │
│ … ┆ … ┆ … ┆ … ┆ … │
│ 2025-12-01 ┆ 3338.33 ┆ -1.194338 ┆ 1995.68 ┆ -1.399209 │
│ 2025-12-02 ┆ 3341.06 ┆ 0.081744 ┆ 1998.79 ┆ 0.155715 │
│ 2025-12-03 ┆ 3334.32 ┆ -0.201936 ┆ 1986.95 ┆ -0.59412 │
│ 2025-12-04 ┆ 3398.21 ┆ 1.898006 ┆ 1973.22 ┆ -0.693407 │
│ 2025-12-05 ┆ 3362.56 ┆ -1.054623 ┆ 1962.18 ┆ -0.561063 │
└────────────┴────────────┴───────────┴───────────┴───────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上のMSVモデルを以下の通り書き下します&lt;sup id="fnref:3"&gt;&lt;a href="#fn:3" class="footnote-ref" role="doc-noteref"&gt;3&lt;/a&gt;&lt;/sup&gt;。事前分布はKim, Shephard and
Chib (1998), 大森 (2019) にならっています。&lt;/p&gt;
&lt;details&gt;&lt;summary&gt;Stanコード（クリックすると折りたたみが開きます）&lt;/summary&gt;
&lt;pre&gt;&lt;code class="language-stan"&gt;// Stanの収束をよくするためのテクニックを以下の通り入れている
// 1. phiのlower, upperを-0.999から0.999に縛っている
// -1 - 1だと両端でサンプリングが不安定になって収束が悪いため
//（sigma系のパラメータのacfやESSが微妙になる）
// 2. h_raw, g_rawの非中心パラメータ化（「再パラメータ化」の一種）
// sigma_etaが小さいとき、hとsigma_etaの事後分布が強く相関するので、h_rawとsigma_etaを分離する
data {
int&amp;lt;lower=0&amp;gt; n; // 時点数
int&amp;lt;lower=1&amp;gt; p; // 次元（p=2 であることを前提としている）
matrix[p, n] y; // リターン（2×n）
}
parameters {
matrix[p, n] h_raw;
vector[n] g_raw;
vector[p] mu;
vector&amp;lt;lower=0.0005, upper=0.9995&amp;gt;[p] phi_raw;
vector&amp;lt;lower=0&amp;gt;[p] sigma_eta_sq;
real&amp;lt;lower=0&amp;gt; sigma_zeta;
}
transformed parameters {
vector&amp;lt;lower=-1, upper=1&amp;gt;[p] phi = 2 * phi_raw - 1;
vector&amp;lt;lower=0&amp;gt;[p] sigma_eta = sqrt(sigma_eta_sq);
matrix[p, n] h;
vector[n] g;
vector&amp;lt;lower=-1, upper=1&amp;gt;[n] rho;
// 非中心パラメータ化
for (i in 1:p) {
h[i, 1] = mu[i] + (sigma_eta[i] / sqrt(1 - phi[i]^2)) * h_raw[i, 1];
}
g[1] = 10 * g_raw[1];
for (t in 2:n) {
for (i in 1:p) {
h[i, t] = mu[i] + phi[i] * (h[i, t-1] - mu[i]) + sigma_eta[i] * h_raw[i, t];
}
g[t] = g[t-1] + sigma_zeta * g_raw[t];
}
for (t in 1:n) {
rho[t] = tanh(g[t]);
}
}
model {
// 論文のとおり事前分布を設定する
// sigma_zetaは論文とはモデルが違うのでflat priorにしている
mu ~ normal(0, 1);
phi_raw ~ beta(20, 1.5);
sigma_eta_sq ~ inv_gamma(2.5, 0.025);
to_vector(h_raw) ~ std_normal();
g_raw ~ std_normal();
// 観測方程式
for (t in 1:n) {
real sigma1 = exp(h[1, t] / 2.0);
real sigma2 = exp(h[2, t] / 2.0);
matrix[p, p] Sigma;
Sigma[1, 1] = sigma1^2;
Sigma[2, 2] = sigma2^2;
Sigma[1, 2] = rho[t] * sigma1 * sigma2;
Sigma[2, 1] = Sigma[1, 2];
y[, t] ~ multi_normal(rep_vector(0.0, p), Sigma);
}
}
generated quantities {
matrix[p, n] volatility;
vector[n] log_lik;
for (t in 1:n) {
real sigma1 = exp(h[1, t] / 2.0);
real sigma2 = exp(h[2, t] / 2.0);
volatility[1, t] = sigma1;
volatility[2, t] = sigma2;
matrix[p, p] Sigma;
Sigma[1, 1] = sigma1^2;
Sigma[2, 2] = sigma2^2;
Sigma[1, 2] = rho[t] * sigma1 * sigma2;
Sigma[2, 1] = Sigma[1, 2];
log_lik[t] = multi_normal_lpdf(y[, t] | rep_vector(0.0, p), Sigma);
}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/details&gt;
&lt;p&gt;MSVモデルは素朴に実装するとMCMCの収束が悪くなりがちなので、非中心パラメータ化のようなテクニックを使っています（Stanコードのコメントを参照）。&lt;/p&gt;
&lt;p&gt;cmdstanpyの&lt;code&gt;CmdStanModel&lt;/code&gt;の&lt;code&gt;sample&lt;/code&gt;を用いて、iter_warmup=1000,
iter_sampling=1000, thin=1,
chain=4で、私の環境では4chain並列して15分くらいで推定できました。なお、使用した環境はPython=3.13.7,
CmdStan=2.36, cmdstanpy=1.3.0です。&lt;/p&gt;
&lt;p&gt;MCMCサンプリングが収束したこと（パラメータ推定に問題がないこと）を確認しましたが、割愛します&lt;sup id="fnref:4"&gt;&lt;a href="#fn:4" class="footnote-ref" role="doc-noteref"&gt;4&lt;/a&gt;&lt;/sup&gt;。&lt;/p&gt;
&lt;h2 id="結果"&gt;結果&lt;/h2&gt;
&lt;p&gt;結果の相関係数です。線の上下の帯は95%ベイズ信用区間です。&lt;/p&gt;
&lt;p&gt;&lt;img src="index_files/figure-commonmark/cell-6-output-1.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;おおむね0.4程度を取っていますが、2016-2020年や2025年は0.2程度まで低下していますね。&lt;/p&gt;
&lt;p&gt;2023年時点での過去10年の月次相関係数を計算すると、国内株と国内REITは0.4程度とのことです。（参考:
&lt;a href="https://money-bu-jpx.com/news/article050020/" target="_blank" rel="noopener noreferrer"&gt;資産配分を再考、最適な投信の組み合わせは？ |
東証マネ部！&lt;/a&gt;）&lt;/p&gt;
&lt;p&gt;これと整合的な結果が得られましたが、時期によってはそれより小さいことも大きいこともあることが分かり、市場の変化に追随できることを示せました。また、各時点での相関係数の信用区間を示すこともでき、不確実性の程度が分かるのもよい点です。&lt;/p&gt;
&lt;p&gt;基本的に国内株も国内REITも金利と逆相関の動きをしますが、株は景気や業績の影響も強く受けるため、2025年のように金利上昇局面でも買われることがありますね。このような場合に相関関係が崩れるのだと思われます。&lt;/p&gt;
&lt;h2 id="補足"&gt;補足&lt;/h2&gt;
&lt;p&gt;このプロットは、冒頭のローリング相関のプロットと異なり、滑らかな曲線を描いています。&lt;/p&gt;
&lt;p&gt;これは、一つはMSVモデルによる相関係数のモデリングの効果によります。もう一つは、MSVモデルでは$t$の相関係数を全期間のリターンデータを用いて推定していますが、ローリング相関では$t$までのデータ（さらに直近n期間のみ）から求めていることの違いによるものです。&lt;/p&gt;
&lt;p&gt;状態空間モデルの文脈で言うと、前者は「平滑化」と呼ばれる推定量に近いものであり、後者は「フィルタ化」と呼ばれる推定量に近いです（ただし、ローリング相関は状態空間モデルではないので、あくまで$t$の相関係数を$t$までのデータから求めているという意味でフィルタ化に近いということです）。&lt;/p&gt;
&lt;p&gt;MCMCで求めたパラメータの推定値は平滑化推定量に近いものです。将来の時点のデータを使って過去の時点のパラメータを求めているためバックテストには使えませんが、後から振り返ってデータを解釈する目的では適していますね。&lt;/p&gt;
&lt;p&gt;バックテストに使うためには、MCMCで毎日回すか、あるいは粒子フィルタのようなフィルタ系の手法で推定してフィルタ化推定量を求めるという解決策があります。後者ですが、推定にかかる時間もフィルタ系の方が早いので、毎日や毎週のオンライン予測には適しています。&lt;/p&gt;
&lt;p&gt;なお、資産価格の系列数が多くなると共分散行列の要素数が多くなるためパラメータの推定が困難になります。その場合は少ない因子に分解して推定するなどの方法を取ります。これも大森
(2019) を参照してください。&lt;/p&gt;
&lt;h2 id="おわりに"&gt;おわりに&lt;/h2&gt;
&lt;p&gt;金融工学の論文は実装すればすぐ収益に結びつくようなものではありませんが、論文の読解と実装を地道に積み重ねていくことで知見が得られると思っています。やはり、巨人の肩の上に立つことには意味があるんだと思います。&lt;/p&gt;
&lt;h2 id="参考文献"&gt;参考文献&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;大森裕浩 (2019). 多変量ボラティリティモデルのベイズ推定.
日本統計学会誌, 48(2), 177-198.&lt;/li&gt;
&lt;li&gt;Aguilar, O., &amp;amp; West, M. (2000). Bayesian dynamic factor models and
portfolio allocation. &lt;em&gt;Journal of Business and Economic Statistics&lt;/em&gt;,
18(3), 338-357.&lt;/li&gt;
&lt;li&gt;Kim, S., Shephard, N., &amp;amp; Chib, S. (1998). Stochastic volatility:
Likelihood inference and comparison with ARCH models. &lt;em&gt;Review of
Economic Studies&lt;/em&gt;, 65(3), 361-393.&lt;/li&gt;
&lt;li&gt;Zhou, X., Nakajima, J., &amp;amp; West, M. (2014). Bayesian forecasting and
portfolio decisions using dynamic dependent sparse factor models.
&lt;em&gt;International Journal of Forecasting&lt;/em&gt;, 30(4), 963-980.&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="footnotes" role="doc-endnotes"&gt;
&lt;hr&gt;
&lt;ol&gt;
&lt;li id="fn:1"&gt;
&lt;p&gt;他に、分足データを用いて相関係数の推定値を求めるRealized
Correlationという手法もあります。&lt;a href="../realized-volatility/"&gt;2024年のアドカレで書いたRealized
Volatilityの記事&lt;/a&gt;の多変量版の拡張になります。&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:2"&gt;
&lt;p&gt;ここで書いたモデルは、大森 (2019)
の「動学的均一相関MSVモデル」を2変量に限定して、$g$をAR(1)ではなくランダムウォークとし、かつ相関係数が正だけではなく負の値も取れるように拡張したものにおおむね相当します。&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:3"&gt;
&lt;p&gt;なお、TOPIXのリターンは平均0.020, 標準偏差1.345,
東証REIT指数のリターンは平均0.005,
標準偏差1.336であり、有意に正でも負でもないため、前掲のモデルの第1式に定数項を入れないことは妥当な定式化と言えます。&amp;#160;&lt;a href="#fnref:3" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:4"&gt;
&lt;p&gt;内容はこちらを参考にしてください: &lt;a href="https://ill-identified.hatenablog.com/entry/2019/06/13/010510" target="_blank" rel="noopener noreferrer"&gt;[R] [stan] bayesplot
を使ったモンテカルロ法の実践ガイド - ill-identified
diary&lt;/a&gt;,
&lt;a href="https://ill-identified.hatenablog.com/entry/2020/05/21/001158" target="_blank" rel="noopener noreferrer"&gt;[R][Stan]マルコフ連鎖モンテカルロ法の実践ガイド2:
ランクプロット他 - ill-identified
diary&lt;/a&gt;&amp;#160;&lt;a href="#fnref:4" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</description></item><item><title>plotnineで非営業日を軸から除いたプロットを描く</title><link>https://suzunano.net/posts/plotnine-non-business-day/</link><pubDate>Sun, 21 Sep 2025 00:00:00 +0900</pubDate><guid>https://suzunano.net/posts/plotnine-non-business-day/</guid><description>&lt;p&gt;Pythonのプロット描画ライブラリであるplotnineにおいて、土日祝日などの任意の非営業日を除外して営業日ベースでプロットを描く方法です。&lt;/p&gt;
&lt;p&gt;結論からいうと、日付からint型の連番列を作ってこの連番列をx軸に取り、&lt;code&gt;plotnine.scale_x_continuous&lt;/code&gt;の&lt;code&gt;labels&lt;/code&gt;引数に軸ラベルにするstr型の日付の文字列を渡せばよいです&lt;sup id="fnref:1"&gt;&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref"&gt;1&lt;/a&gt;&lt;/sup&gt;。&lt;/p&gt;
&lt;p&gt;株価のようにデータによっては非営業日を除外して軸を描くケースが頻出なんですよね。よく使うのでメモしておきます。&lt;/p&gt;
&lt;p&gt;環境は以下のとおりです。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Python=3.13.7&lt;/li&gt;
&lt;li&gt;polars=1.33.1&lt;/li&gt;
&lt;li&gt;plotnine=0.15.0&lt;/li&gt;
&lt;li&gt;mizani=0.14.2&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;こういうサンプルデータを考えます。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-python"&gt;import polars as pl
import plotnine as p9
from mizani.breaks import breaks_width, breaks_date_width
df = (
# 2025/1/11-12は土日、1/13は祝日、1/18-19は土日
pl.DataFrame({
&amp;quot;date&amp;quot;: [
&amp;quot;2025-01-06&amp;quot;, &amp;quot;2025-01-07&amp;quot;, &amp;quot;2025-01-08&amp;quot;, &amp;quot;2025-01-09&amp;quot;, &amp;quot;2025-01-10&amp;quot;,
&amp;quot;2025-01-14&amp;quot;, &amp;quot;2025-01-15&amp;quot;, &amp;quot;2025-01-16&amp;quot;, &amp;quot;2025-01-17&amp;quot;,
&amp;quot;2025-01-20&amp;quot;, &amp;quot;2025-01-21&amp;quot;, &amp;quot;2025-01-22&amp;quot;, &amp;quot;2025-01-23&amp;quot;, &amp;quot;2025-01-24&amp;quot;
],
&amp;quot;a&amp;quot;: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14],
&amp;quot;b&amp;quot;: [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
})
.with_columns(
date=pl.col(&amp;quot;date&amp;quot;).str.strptime(pl.Date, &amp;quot;%Y-%m-%d&amp;quot;),
)
)
df = (
df
.unpivot(
on=[&amp;quot;a&amp;quot;, &amp;quot;b&amp;quot;],
index=&amp;quot;date&amp;quot;,
variable_name=&amp;quot;stock_name&amp;quot;,
value_name=&amp;quot;price&amp;quot;
)
.sort(&amp;quot;date&amp;quot;, &amp;quot;stock_name&amp;quot;)
)
print(df)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;shape: (28, 3)
┌────────────┬────────────┬───────┐
│ date ┆ stock_name ┆ price │
│ --- ┆ --- ┆ --- │
│ date ┆ str ┆ i64 │
╞════════════╪════════════╪═══════╡
│ 2025-01-06 ┆ a ┆ 1 │
│ 2025-01-06 ┆ b ┆ 2 │
│ 2025-01-07 ┆ a ┆ 2 │
│ 2025-01-07 ┆ b ┆ 3 │
│ 2025-01-08 ┆ a ┆ 3 │
│ … ┆ … ┆ … │
│ 2025-01-22 ┆ b ┆ 13 │
│ 2025-01-23 ┆ a ┆ 13 │
│ 2025-01-23 ┆ b ┆ 14 │
│ 2025-01-24 ┆ a ┆ 14 │
│ 2025-01-24 ┆ b ┆ 15 │
└────────────┴────────────┴───────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;a&lt;/code&gt;と&lt;code&gt;b&lt;/code&gt;という株の株価が記録された&lt;code&gt;polars.DataFrame&lt;/code&gt;だとイメージしてください。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;aes&lt;/code&gt;の&lt;code&gt;x&lt;/code&gt;に&lt;code&gt;date&lt;/code&gt;を指定してふつうに折れ線グラフを描くと、レコードがない日、すなわち株式市場が空いていない日が直線で繋がってしまいます&lt;sup id="fnref:2"&gt;&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref"&gt;2&lt;/a&gt;&lt;/sup&gt;。そうではなく、例えば2025/1/10の一つ隣は2025/1/14が来るようにプロットしたいです。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-python"&gt;(
p9.ggplot(
df,
p9.aes(x=&amp;quot;date&amp;quot;, y=&amp;quot;price&amp;quot;, color=&amp;quot;stock_name&amp;quot;)
) +
p9.geom_line() +
p9.geom_point() +
p9.scale_x_date(breaks=breaks_date_width(&amp;quot;1 day&amp;quot;), minor_breaks=None) +
p9.scale_y_continuous(breaks=breaks_width(1), minor_breaks=None) +
p9.theme(axis_text_x=p9.element_text(rotation=90))
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src="index_files/figure-commonmark/cell-4-output-1.png"
width="1600" height="800" /&gt;&lt;/p&gt;
&lt;p&gt;これを解決するには、まずx軸に取りたい日付の&lt;code&gt;date&lt;/code&gt;をintの連番にしたindex列を用意します。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-python"&gt;df = (
df
# 縦持ちのデータでは同一日が複数行あるので、rankのmethodはdenseを使う
.with_columns(
date_idx=pl.col(&amp;quot;date&amp;quot;).rank(&amp;quot;dense&amp;quot;) - 1
)
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;date_idx&lt;/code&gt;で1を引いているのは、&lt;code&gt;date_idx&lt;/code&gt;を0始まりにするためです。あとで5日おきに軸ラベルを振るコードを示しますが、そのとき5で割り切れる&lt;code&gt;date_idx&lt;/code&gt;に軸ラベルを振るというコードで最初の日付にもラベルを振ることができ、コードが分かりやすくなります&lt;sup id="fnref:3"&gt;&lt;a href="#fn:3" class="footnote-ref" role="doc-noteref"&gt;3&lt;/a&gt;&lt;/sup&gt;。&lt;/p&gt;
&lt;p&gt;そしてindex列をx軸に取り、&lt;code&gt;scale_x_continuous&lt;/code&gt;の&lt;code&gt;breaks&lt;/code&gt;と&lt;code&gt;minor_breaks&lt;/code&gt;引数にそれぞれ軸ラベルと目盛りを振りたい連番を指定してあげます。&lt;/p&gt;
&lt;p&gt;また、軸ラベルにする日付の文字列として、&lt;code&gt;breaks&lt;/code&gt;の引数と同じ長さの&lt;code&gt;list[str]&lt;/code&gt;を&lt;code&gt;labels&lt;/code&gt;に与えます。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-python"&gt;date_labels = sorted(df[&amp;quot;date&amp;quot;].unique().dt.strftime(&amp;quot;%Y-%m-%d&amp;quot;).to_list())
date_idx = sorted(df[&amp;quot;date_idx&amp;quot;].unique().to_list())
# 軸ラベルは全部の日付に振り、軸ラベルの間のminor_breaksは振らないとする
major_breaks_idx = date_idx
minor_breaks_idx = None
major_labels = [date_labels[i] for i in major_breaks_idx]
(
p9.ggplot(
df,
p9.aes(x=&amp;quot;date_idx&amp;quot;, y=&amp;quot;price&amp;quot;, color=&amp;quot;stock_name&amp;quot;)
) +
p9.geom_line() +
p9.geom_point() +
p9.scale_x_continuous(breaks=major_breaks_idx, minor_breaks=minor_breaks_idx, labels=major_labels) +
p9.scale_y_continuous(breaks=breaks_width(1), minor_breaks=None) +
p9.theme(axis_text_x=p9.element_text(rotation=90)) +
p9.labs(x=&amp;quot;date&amp;quot;)
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src="index_files/figure-commonmark/cell-6-output-1.png"
width="1600" height="800" /&gt;&lt;/p&gt;
&lt;p&gt;うまく描けていますね。&lt;/p&gt;
&lt;p&gt;実際は軸ラベルを整えたいことも多いですが、5日おきに軸ラベルを振るということもできます。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-python"&gt;major_breaks_idx = [i for i in date_idx if i % 5 == 0]
minor_breaks_idx = date_idx
major_labels = [date_labels[i] for i in major_breaks_idx]
(
p9.ggplot(
df,
p9.aes(x=&amp;quot;date_idx&amp;quot;, y=&amp;quot;price&amp;quot;, color=&amp;quot;stock_name&amp;quot;)
) +
p9.geom_line() +
p9.geom_point() +
p9.scale_x_continuous(breaks=major_breaks_idx, minor_breaks=minor_breaks_idx, labels=major_labels) +
p9.scale_y_continuous(breaks=breaks_width(1), minor_breaks=None) +
p9.theme(axis_text_x=p9.element_text(rotation=90)) +
p9.labs(x=&amp;quot;date&amp;quot;)
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src="index_files/figure-commonmark/cell-7-output-1.png"
width="1600" height="800" /&gt;&lt;/p&gt;
&lt;p&gt;例えば月曜日を週始まりとして、週の最初の日付だけに軸ラベルを振りたいというケースもよくあります。&lt;/p&gt;
&lt;p&gt;この場合も、&lt;code&gt;scale_x_continuous&lt;/code&gt;の&lt;code&gt;breaks&lt;/code&gt;引数に、軸ラベルを振りたい&lt;code&gt;date&lt;/code&gt;の連番を渡せばよいです。polarsで各週の最初の曜日の日付のindexを集計して、それを&lt;code&gt;scale_x_continuous&lt;/code&gt;の&lt;code&gt;breaks&lt;/code&gt;引数に渡せばよいですね。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-python"&gt;idx_first_business_day_of_week = (
df
.with_columns(
year=pl.col(&amp;quot;date&amp;quot;).dt.year(),
week=pl.col(&amp;quot;date&amp;quot;).dt.week()
)
.group_by(&amp;quot;year&amp;quot;, &amp;quot;week&amp;quot;)
.agg(
first_business_day_of_week_idx=pl.col(&amp;quot;date_idx&amp;quot;).min()
)
.sort(&amp;quot;year&amp;quot;, &amp;quot;week&amp;quot;)
.get_column(&amp;quot;first_business_day_of_week_idx&amp;quot;)
.to_list()
)
major_breaks_idx = idx_first_business_day_of_week
minor_breaks_idx = date_idx
major_labels = [date_labels[i] for i in major_breaks_idx]
(
p9.ggplot(
df,
p9.aes(x=&amp;quot;date_idx&amp;quot;, y=&amp;quot;price&amp;quot;, color=&amp;quot;stock_name&amp;quot;)
) +
p9.geom_line() +
p9.geom_point() +
p9.scale_x_continuous(breaks=major_breaks_idx, minor_breaks=minor_breaks_idx, labels=major_labels) +
p9.scale_y_continuous(breaks=breaks_width(1), minor_breaks=None) +
p9.theme(axis_text_x=p9.element_text(rotation=90)) +
p9.labs(x=&amp;quot;date&amp;quot;)
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src="index_files/figure-commonmark/cell-8-output-1.png"
width="1600" height="800" /&gt;&lt;/p&gt;
&lt;p&gt;注意点として、年をまたぐ同一の週番号を正しく別に扱えるように&lt;code&gt;year&lt;/code&gt;でも&lt;code&gt;group_by&lt;/code&gt;して集計しています。&lt;/p&gt;
&lt;p&gt;ISO
weekの週は月曜日始まりなので、&lt;code&gt;week&lt;/code&gt;で&lt;code&gt;group_by&lt;/code&gt;して&lt;code&gt;date_idx&lt;/code&gt;の&lt;code&gt;min&lt;/code&gt;を取ると、月曜日始まりの週の最初の日付を取得できます。月曜日以外の日を週始まりとしたい場合は別の書き方が必要ですが、そうしたいケースに出会ったことはないのでたいていこれで十分だと思います。&lt;/p&gt;
&lt;p&gt;同様に月の最初の日だけ軸ラベルを振るようなこともできます。上のコードとほとんど同じコードなので省略しますが、この場合は&lt;code&gt;week&lt;/code&gt;を&lt;code&gt;month&lt;/code&gt;に読み替えればOKです。&lt;/p&gt;
&lt;div class="footnotes" role="doc-endnotes"&gt;
&lt;hr&gt;
&lt;ol&gt;
&lt;li id="fn:1"&gt;
&lt;p&gt;ちなみにRのggplot2を使う場合は、&lt;a href="https://github.com/dvmlls/bdscale" target="_blank" rel="noopener noreferrer"&gt;bdscale&lt;/a&gt;という便利なパッケージがあり、&lt;code&gt;bdscale::scale_x_bd&lt;/code&gt;で一発で描けます。&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:2"&gt;
&lt;p&gt;&lt;code&gt;df&lt;/code&gt;は株式市場が空いていない日はレコードがありませんが、例えば空いていない日を&lt;code&gt;date&lt;/code&gt;列に入れて&lt;code&gt;a&lt;/code&gt;列と&lt;code&gt;b&lt;/code&gt;列を&lt;code&gt;None&lt;/code&gt;としたとしても、直線が途中で切れるだけで所望のプロットは描けません。&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:3"&gt;
&lt;p&gt;1始まりでも「5で割って1余る&lt;code&gt;date_idx&lt;/code&gt;に軸ラベルを振る」とできますが、ちょっと分かりにくい気がします。&amp;#160;&lt;a href="#fnref:3" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</description></item><item><title>機械学習によるレコメンドエンジンで自分に小説をおすすめした</title><link>https://suzunano.net/posts/book-recommend/</link><pubDate>Tue, 15 Jul 2025 00:00:00 +0900</pubDate><guid>https://suzunano.net/posts/book-recommend/</guid><description>&lt;h2 id="はじめに"&gt;はじめに&lt;/h2&gt;
&lt;p&gt;この一年ほど、小説をよく読んでいます。それまでは一年に一冊も小説を読まなかったのですが、一度読んでみるとだんだん自分の好みが分かってきて楽しくなりました。&lt;/p&gt;
&lt;p&gt;すると、自分の好みに合う小説や、以前読んで気に入った小説に似た小説が読みたくなります。Amazonの「この商品をチェックした人はこんな商品もチェックしています」のレコメンドを参考に新しい本を知ることが多いです。&lt;/p&gt;
&lt;p&gt;しかし、Amazonが作ったブラックボックスのアルゴリズムに自分の興味を操作されるのはなんか嫌ですね。そこで機械学習によるレコメンドエンジンを作って自分に本をおすすめすることにしました。&lt;/p&gt;
&lt;h2 id="スクレイピング"&gt;スクレイピング&lt;/h2&gt;
&lt;p&gt;レコメンドエンジンを作るためのレビューデータとして、&lt;a href="https://bookmeter.com/" target="_blank" rel="noopener noreferrer"&gt;読書メーター&lt;/a&gt;をスクレイピングしました。&lt;/p&gt;
&lt;p&gt;読書メーターではユーザが読んだ本を登録することができます。この、それぞれのユーザがどの本を何回「読んだ本」リストに登録したかというデータを用います。「AさんはX,
Y,
Zの3冊を読んだ」というようなデータです。本の作家の名前やあらすじなど、それ以外の情報は用いていません。&lt;/p&gt;
&lt;p&gt;ユーザIDが&lt;code&gt;user_id&lt;/code&gt;のユーザの読んだ本は、&lt;code&gt;https://bookmeter.com/users/{user_id}/books/read&lt;/code&gt;で見ることができます。&lt;code&gt;user_id&lt;/code&gt;は1から始まる自然数であり、約150万まで存在しました。この中から20%分のIDをサンプリングして、十分に間隔を空けながらforループで上のページをスクレイピングしました。&lt;/p&gt;
&lt;p&gt;その結果、IDが存在しないユーザ（おそらく退会したユーザ）や、1冊も読んでいないユーザを除外して、168,262人のユーザによる1,679,143冊の本に対する22,188,378件のレビューを得ることができました。&lt;/p&gt;
&lt;p&gt;さて、今回取得した168,262人のユーザに最も読まれた本は何でしょうか？&lt;/p&gt;
&lt;div&gt;&lt;style&gt;
.dataframe &gt; thead &gt; tr,
.dataframe &gt; tbody &gt; tr {
text-align: right;
white-space: pre-wrap;
}
&lt;/style&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;title&lt;/th&gt;
&lt;th&gt;author&lt;/th&gt;
&lt;th&gt;count&lt;/th&gt;
&lt;th&gt;user_count&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&amp;ldquo;阪急電車 (幻冬舎文庫)&amp;rdquo;&lt;/td&gt;
&lt;td&gt;&amp;ldquo;有川 浩&amp;rdquo;&lt;/td&gt;
&lt;td&gt;12368&lt;/td&gt;
&lt;td&gt;11712&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;ldquo;夜は短し歩けよ乙女 (角川文庫 も 19-2)&amp;rdquo;&lt;/td&gt;
&lt;td&gt;&amp;ldquo;森見 登美彦&amp;rdquo;&lt;/td&gt;
&lt;td&gt;10862&lt;/td&gt;
&lt;td&gt;10318&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;ldquo;西の魔女が死んだ (新潮文庫)&amp;rdquo;&lt;/td&gt;
&lt;td&gt;&amp;ldquo;梨木 香歩&amp;rdquo;&lt;/td&gt;
&lt;td&gt;10587&lt;/td&gt;
&lt;td&gt;10090&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p&gt;「&lt;a href="https://www.amazon.co.jp/dp/4344415132" target="_blank" rel="noopener noreferrer"&gt;阪急電車&lt;/a&gt;」でした。11,712人のユーザに合計12,368回読まれた本でした。ユーザ数より読まれた回数が多いのは、読書メーターでは同一の本を複数回読んだと登録できるためです。&lt;/p&gt;
&lt;h2 id="implicit-matrix-factorizationのアルゴリズム"&gt;Implicit Matrix Factorizationのアルゴリズム&lt;/h2&gt;
&lt;p&gt;今回のレコメンドエンジンで使うImplicit Matrix
Factorizationについて説明します。この分野は全くの素人なので誤りがあるかもしれません。&lt;/p&gt;
&lt;p&gt;この節の説明は&lt;a href="https://www.oreilly.co.jp/books/9784873119663/" target="_blank" rel="noopener noreferrer"&gt;推薦システム実践入門&lt;/a&gt;を参考にしました。体系的にレコメンドエンジンを学ぶことができ、理論と実装の説明のバランスが取れているかなりの良書でした。&lt;/p&gt;
&lt;h3 id="明示的評価値と暗黙的評価値"&gt;明示的評価値と暗黙的評価値&lt;/h3&gt;
&lt;p&gt;いま、ユーザ$u (1, \dots, n)$のアイテム$i (1, \dots, m)$に対する評価値を$r_{u,i}$とします。$r_{u,i}$を行方向にユーザ、列方向にアイテムを取って並べた$n \times m$の行列を評価値行列と呼びます。以下、$R$と表記します。&lt;/p&gt;
&lt;p&gt;この評価値には、明示的評価値と暗黙的評価値の二種類があります。&lt;/p&gt;
&lt;p&gt;明示的評価値とは、ユーザが直接アイテムに得点を付けたような評価値を指します。Amazonのレビューの点数のようなものですね。一方、暗黙的評価値とは、ユーザがアイテムに対して起こした行動に関するデータです。例えばECサイトでのアイテムの閲覧回数です。今回用いる読書メーターの評価値は、ユーザがそれぞれの本を読んだと登録した回数であり、暗黙的評価値にあたります。&lt;/p&gt;
&lt;p&gt;明示的評価値はユーザの好みを正確に示したデータですが、暗黙的評価値はユーザの好みをそのまま反映しているとは限りません。そのため、各種のレコメンド手法は、明示的評価値に適用できるものと暗黙的評価値に適用できるものに分かれます。&lt;/p&gt;
&lt;h3 id="行列分解によるレコメンドエンジン"&gt;行列分解によるレコメンドエンジン&lt;/h3&gt;
&lt;p&gt;評価値行列$R$を、ユーザの特徴を表すユーザ行列とアイテムの特徴を表すアイテム行列という二つの行列に分解することを考えます。具体的には、$R$を$R = PQ^{T}$で表される行列$P (n \times k)$,
$Q (m \times k)$に分解します。&lt;/p&gt;
&lt;p&gt;$P$をユーザ行列、$Q$をアイテム行列と呼びます。$k$は潜在因子数というハイパーパラメータです。この操作は、ユーザ$u$とアイテム$i$をそれぞれ$k$次元のベクトルで表現するということです。$k$は大きい値にするほど表現力が高くなりますが、過学習しやすくなります。&lt;/p&gt;
&lt;p&gt;以上より、ユーザ$u$のアイテム$i$に対する評価値$r_{u,i}$の予測値はベクトルの内積$P_{u} Q_{i}^{T}$で求められます。評価値の予測値が高いものをユーザにレコメンドします。&lt;/p&gt;
&lt;h3 id="implicit-matrix-factorization"&gt;Implicit Matrix Factorization&lt;/h3&gt;
&lt;p&gt;Implicit Matrix
Factorizationは、暗黙的評価値に対して適用できる、行列分解による協調フィルタリングベースのレコメンド手法の一つです。&lt;/p&gt;
&lt;p&gt;概要を簡単に説明します。以下、$r_{u,i}$を暗黙的評価値とします。&lt;/p&gt;
&lt;p&gt;$\bar{r}_{u,i}$を$\bar{r}_{u,i} = 1 (r_{u,i} &amp;gt; 0), 0 (r_{u,i} = 0)$で定義します。$r_{u,i}$が0より大きい正の値であれば、ユーザ$u$はアイテム$i$に対して好意を持っていることを示します。$\bar{r}_{u,i}$は、好意を持っているかどうかを示す0/1の変数です。&lt;/p&gt;
&lt;p&gt;$c_{u,i} = 1 + \alpha r_{u,i}$で定義される$c_{u,i}$を信頼度と呼びます。&lt;/p&gt;
&lt;p&gt;$r_{u,i} = 0$の場合、ユーザ$u$はアイテム$i$に対して好意を持っていないとは限りません。そのアイテムを知らなかっただけの可能性もあるからです。そのため、$r_{u,i} = 0$の場合でも$c_{u,i} = 1$を割り当てます。&lt;/p&gt;
&lt;p&gt;$r_{u,i}$が大きければ大きいほどユーザ$u$はアイテム$i$に対して大きな好意を持っていると考えられますが、暗黙的評価値である$r_{u,i}$をそのまま好意の度合いとして使用することは適切ではありません。$r_{u,i}$がどの程度好意を表すかというパラメータ$\alpha$を導入して、好意の信頼度$c_{u,i}$を定義します。&lt;/p&gt;
&lt;p&gt;Implicit Matrix
Factorizationで求められるユーザ行列$P$とアイテム行列$Q$は、以下を満たす行列です。&lt;/p&gt;
&lt;p&gt;$$
min_{p,q} \sum_{u} \sum_{i} c_{u,i} (\bar{r}_{u,i} - p_{u}^{T} q_{i})^{2} + \lambda (\sum_{u} ||p_{u}||^{2} + \sum_{i} ||q_{i}||^{2})
$$&lt;/p&gt;
&lt;p&gt;右辺第2項は過学習防止のためのL2正則化であり、$||p_{u}||^{2} = p_{u,1}^{2} + p_{u,2}^{2} + \dots$（L2ノルム）です。&lt;/p&gt;
&lt;p&gt;この関数を普通に最小化しようとするとユーザ数 x
アイテム数を計算することになり計算コストが大きいですが、implicit
alternating least
squares（iALS）というアルゴリズムを用いるとこの目的関数を効率的に最小化することができます。2008年の論文のモデルですが、&lt;a href="https://dl.acm.org/doi/10.1145/3523227.3548486" target="_blank" rel="noopener noreferrer"&gt;Revisiting
the Performance of iALS on Item Recommendation
Benchmarks&lt;/a&gt;によると、ハイパーパラメータを調整することで2022年時点では深層学習系のモデルと引けを取らない精度が出るそうです。&lt;/p&gt;
&lt;p&gt;アルゴリズムについては参考文献に載せたとおり素晴らしく分かりやすく解説されたサイトがありますので、詳細はそちらをご覧ください。&lt;/p&gt;
&lt;h2 id="実装"&gt;実装&lt;/h2&gt;
&lt;h3 id="評価値行列の作成"&gt;評価値行列の作成&lt;/h3&gt;
&lt;p&gt;環境はPython 3.12.0, polars 1.31.0, implicit 0.7.2です。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-python"&gt;import implicit
import numpy as np
import polars as pl
from scipy import sparse
from threadpoolctl import threadpool_limits
from tqdm import tqdm
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;次のデータを持っています。ユーザ&lt;code&gt;user_id&lt;/code&gt;が本&lt;code&gt;book_id&lt;/code&gt;を&lt;code&gt;count&lt;/code&gt;回読んだというデータであり、&lt;code&gt;user_id&lt;/code&gt;と&lt;code&gt;book_id&lt;/code&gt;の組み合わせ数だけレコードがあります。ただし表示している&lt;code&gt;user_id&lt;/code&gt;は匿名化したものであり、読書メーターの実際の&lt;code&gt;user_id&lt;/code&gt;からは変更しています。なお、読まれた人数が少ない本や、読んだ本の数が少ないユーザによるレビューを除外しています。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;shape: (14_841_848, 3)
┌─────────┬──────────┬───────┐
│ user_id ┆ book_id ┆ count │
╞═════════╪══════════╪═══════╡
│ 1 ┆ 2845 ┆ 1 │
│ 1 ┆ 104674 ┆ 1 │
│ 1 ┆ 105027 ┆ 1 │
│ 1 ┆ 105086 ┆ 1 │
│ 1 ┆ 105096 ┆ 1 │
│ … ┆ … ┆ … │
│ 134877 ┆ 20586079 ┆ 1 │
│ 134877 ┆ 20716260 ┆ 1 │
│ 134877 ┆ 21248535 ┆ 1 │
│ 134877 ┆ 21658085 ┆ 1 │
│ 134877 ┆ 21700595 ┆ 1 │
└─────────┴──────────┴───────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;このDataFrameから評価値行列を作成します。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-python"&gt;user_ids = np.array(sorted(set(df.get_column(&amp;quot;user_id&amp;quot;))))
book_ids = np.array(sorted(set(df.get_column(&amp;quot;book_id&amp;quot;))))
user_id2index = dict(zip(user_ids, range(len(user_ids))))
book_id2index = dict(zip(book_ids, range(len(book_ids))))
index2user_id = dict(zip(range(len(user_ids)), user_ids))
index2book_id = dict(zip(range(len(book_ids)), book_ids))
# implicit.als.AlternatingLeastSquares.fitに通せるのはcsr_matrixだが、
# 行方向の代入はlil_matrixのほうが早いので、lil_matrixで代入してからcsr_matrixに変換する
feature_matrix = sparse.lil_matrix(
np.zeros((len(user_ids), len(book_ids)), dtype=np.int8)
)
# Implicit Matrix Factorizationのalpha
alpha = 1.0
for u, b, c in tqdm(zip(
df.get_column(&amp;quot;user_id&amp;quot;).to_numpy(),
df.get_column(&amp;quot;book_id&amp;quot;).to_numpy(),
df.get_column(&amp;quot;count&amp;quot;).to_numpy()
)):
feature_matrix[
user_id2index[u], book_id2index[b]
] = 1.0 * alpha
feature_matrix = feature_matrix.tocsr()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;同一のユーザが同一の本を2回以上読んでいることがありますが、評価値行列では1回しか読んでいないという扱いにしました。$r_{u,i} \geq 2$の場合は$r_{u,i} = 1$として扱ったということです。読書メーターの「読んだ本」の登録は何回でもできるのですが、たいていのユーザは1回だけ登録している一方、一部のユーザは何回も登録しているようなことがあるため、2回以上読んだ場合でも1回とみなす方が適当に思われたためです。&lt;/p&gt;
&lt;p&gt;134,877人のユーザ x
119,737冊の本の合計14,841,848件のレビューを評価値行列とします。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-python"&gt;feature_matrix
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;Compressed Sparse Row sparse matrix of dtype 'int8'
with 14841848 stored elements and shape (134877, 119737)&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id="モデルの学習"&gt;モデルの学習&lt;/h3&gt;
&lt;p&gt;Pythonでは&lt;code&gt;implicit.als.AlternatingLeastSquares&lt;/code&gt;で実装されているのでこれを使います。&lt;/p&gt;
&lt;p&gt;なお、潜在因子数$k$は$k = 512$としました。いろいろ試してみて、なんとなく妥当な結果だと思えたのが512だからです。評価指標を用いてちゃんと決めたほうがいいです。&lt;/p&gt;
&lt;p&gt;また、その他のハイパーパラメータはデフォルト値のままですが、先の論文によるとハイパーパラメータチューニングによって大きく性能が変わるようなのでこれもちゃんとチューニングしたほうがいいです。論文によれば、iALSはまずは$k$をできるだけ大きく取り、次に正則化パラメータ$\lambda$を調整することでよい精度が出るそうです。（ハイパーパラメータのチューニングは別の記事にするかもしれません）。&lt;/p&gt;
&lt;p&gt;i9-9900K（16スレッド）でスレッド並列で実行すると2分くらいで計算が終わります。データセットの規模の割に高速ですね。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-python"&gt;# これを実行するとマルチスレッド環境でimplicit.als.AlternatingLeastSquaresの計算速度が落ちない
threadpool_limits(1, &amp;quot;blas&amp;quot;)
model = implicit.als.AlternatingLeastSquares(
factors=512,
regularization=0.01, # デフォルト値
iterations=10,
calculate_training_loss=True,
random_state=1,
)
# 元のfeature_matrixでモデルを再学習（新しいユーザ推薦用）
model.fit(feature_matrix)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id="レコメンド"&gt;レコメンド&lt;/h3&gt;
&lt;p&gt;実際にレコメンドを出してみます。&lt;/p&gt;
&lt;p&gt;インプットにしたい本を評価値行列にして、&lt;code&gt;partial_fit_users&lt;/code&gt;というメソッドで学習済みモデルから埋め込みベクトルを作り、&lt;code&gt;recommend&lt;/code&gt;でレコメンドを出せます。このときすべてのデータセットで再学習する必要はなく、1秒もしないうちに結果が出るので一度学習済みモデルを作ってしまえばとても使い勝手が良いです。&lt;/p&gt;
&lt;p&gt;わたしは綿矢りささんが好きです。女性の割り切れない感情を繊細で美しい文章で表現するところが好きです。というわけでまずは綿矢さんの「&lt;a href="https://www.amazon.co.jp/dp/4167840022" target="_blank" rel="noopener noreferrer"&gt;かわいそうだね？&lt;/a&gt;」に対するレコメンドを出してみます。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-python"&gt;# 新しいユーザの評価値行列を付ける（今レコメンドを出したいユーザ）
new_book_ids = [4255880]
new_feature_matrix = sparse.lil_matrix(
np.zeros((1, len(book_ids)), dtype=np.int8)
)
for nbi in new_book_ids:
new_feature_matrix[0, book_id2index[nbi]] = 1.0 * alpha
new_feature_matrix = new_feature_matrix.tocsr()
# 新しいユーザのインデックス
new_user_index = feature_matrix.shape[0]
# 新しいユーザをモデルに追加学習させる
model.partial_fit_users([new_user_index], new_feature_matrix)
# 新しいユーザに対するレコメンド
ids, scores = model.recommend(
userid=new_user_index,
user_items=new_feature_matrix,
N=100,
filter_already_liked_items=True
)
res = (
pl.DataFrame({&amp;quot;book_id&amp;quot;: [index2book_id[i] for i in ids], &amp;quot;score&amp;quot;: scores})
.with_columns(book_id=pl.col(&amp;quot;book_id&amp;quot;).cast(pl.Int32))
)
# 別に持っていたbook_idと著者名のマスタテーブルから著者名を付ける
res = (
res
.join(books, on=&amp;quot;book_id&amp;quot;, how=&amp;quot;left&amp;quot;)
.with_columns(score=pl.col(&amp;quot;score&amp;quot;).round(4))
.select(&amp;quot;title&amp;quot;, &amp;quot;author&amp;quot;, &amp;quot;score&amp;quot;)
)
res.head(10)
&lt;/code&gt;&lt;/pre&gt;
&lt;div&gt;&lt;style&gt;
.dataframe &gt; thead &gt; tr,
.dataframe &gt; tbody &gt; tr {
text-align: right;
white-space: pre-wrap;
}
&lt;/style&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;title&lt;/th&gt;
&lt;th&gt;author&lt;/th&gt;
&lt;th&gt;score&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&amp;ldquo;何者&amp;rdquo;&lt;/td&gt;
&lt;td&gt;&amp;ldquo;朝井 リョウ&amp;rdquo;&lt;/td&gt;
&lt;td&gt;0.0207&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;ldquo;勝手にふるえてろ&amp;rdquo;&lt;/td&gt;
&lt;td&gt;&amp;ldquo;綿矢 りさ&amp;rdquo;&lt;/td&gt;
&lt;td&gt;0.0167&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;ldquo;蹴りたい背中 (河出文庫 わ 1-2)&amp;rdquo;&lt;/td&gt;
&lt;td&gt;&amp;ldquo;綿矢 りさ&amp;rdquo;&lt;/td&gt;
&lt;td&gt;0.0161&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;ldquo;推し、燃ゆ&amp;rdquo;&lt;/td&gt;
&lt;td&gt;&amp;ldquo;宇佐見りん&amp;rdquo;&lt;/td&gt;
&lt;td&gt;0.0139&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;ldquo;ひらいて&amp;rdquo;&lt;/td&gt;
&lt;td&gt;&amp;ldquo;綿矢 りさ&amp;rdquo;&lt;/td&gt;
&lt;td&gt;0.0137&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;ldquo;勝手にふるえてろ (文春文庫 わ 17-1)&amp;rdquo;&lt;/td&gt;
&lt;td&gt;&amp;ldquo;綿矢 りさ&amp;rdquo;&lt;/td&gt;
&lt;td&gt;0.0132&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;ldquo;蹴りたい背中&amp;rdquo;&lt;/td&gt;
&lt;td&gt;&amp;ldquo;綿矢 りさ&amp;rdquo;&lt;/td&gt;
&lt;td&gt;0.0125&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;ldquo;ふがいない僕は空を見た&amp;rdquo;&lt;/td&gt;
&lt;td&gt;&amp;ldquo;窪 美澄&amp;rdquo;&lt;/td&gt;
&lt;td&gt;0.0118&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;ldquo;しょうがの味は熱い&amp;rdquo;&lt;/td&gt;
&lt;td&gt;&amp;ldquo;綿矢 りさ&amp;rdquo;&lt;/td&gt;
&lt;td&gt;0.0109&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;ldquo;すべて真夜中の恋人たち&amp;rdquo;&lt;/td&gt;
&lt;td&gt;&amp;ldquo;川上 未映子&amp;rdquo;&lt;/td&gt;
&lt;td&gt;0.0109&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p&gt;似ている度合いのスコアが高い順に並べました。特徴量に作家名は使っていないにもかかわらず、インプットと同じ綿矢りさ作品が多く並んでいるのが驚きです。本を読む人は、読んだ本の別の作家の本を読んだり作家を追っていたりすることが多いので、協調フィルタリングがこのような結果を返すのは納得です。&lt;/p&gt;
&lt;p&gt;なお、同一タイトルの本が複数回登場していますが、単行本と文庫本の違いです。読書メーターでは単行本と文庫本はそれぞれ別の本として存在するためです。レコメンド上はどちらか一つに揃えてもよいのですが、元のデータが分かれているので仕方ないものとしてそのままにしています。&lt;/p&gt;
&lt;p&gt;一方で、別の本として扱うことにメリットもあります。ふつう、最初に単行本で発売され、ある程度売れると文庫本が出ます。そのため、単行本で読んだ人はその作者を追っている熱心なファンである可能性が高く、単行本で読んだか文庫本で読んだかは異なる情報を持っています。特徴量設計の難しいポイントですね。&lt;/p&gt;
&lt;p&gt;複数冊をインプットにしてレコメンドすることもできます。大人のやさしい恋愛小説やささやかな日常をテーマにした短編が好きなので、以下の2冊でレコメンドしてみます。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;畑野智美「&lt;a href="https://www.amazon.co.jp/dp/4122071712" target="_blank" rel="noopener noreferrer"&gt;大人になったら、&lt;/a&gt;」&lt;/li&gt;
&lt;li&gt;加藤千恵「&lt;a href="https://www.amazon.co.jp/dp/419894587X" target="_blank" rel="noopener noreferrer"&gt;消えていく日に&lt;/a&gt;」&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;style&gt;
.dataframe &gt; thead &gt; tr,
.dataframe &gt; tbody &gt; tr {
text-align: right;
white-space: pre-wrap;
}
&lt;/style&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;title&lt;/th&gt;
&lt;th&gt;author&lt;/th&gt;
&lt;th&gt;score&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&amp;ldquo;マカン・マラン - 二十三時の夜食カフェ&amp;rdquo;&lt;/td&gt;
&lt;td&gt;&amp;ldquo;古内 一絵&amp;rdquo;&lt;/td&gt;
&lt;td&gt;0.0053&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;ldquo;わたしたちは銀のフォークと薬を手にして&amp;rdquo;&lt;/td&gt;
&lt;td&gt;&amp;ldquo;島本 理生&amp;rdquo;&lt;/td&gt;
&lt;td&gt;0.0051&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;ldquo;女王さまの夜食カフェ - マカン・マラン ふたたび&amp;rdquo;&lt;/td&gt;
&lt;td&gt;&amp;ldquo;古内 一絵&amp;rdquo;&lt;/td&gt;
&lt;td&gt;0.0049&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;ldquo;神さまを待っている&amp;rdquo;&lt;/td&gt;
&lt;td&gt;&amp;ldquo;畑野 智美&amp;rdquo;&lt;/td&gt;
&lt;td&gt;0.0048&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;ldquo;大人は泣かないと思っていた&amp;rdquo;&lt;/td&gt;
&lt;td&gt;&amp;ldquo;寺地 はるな&amp;rdquo;&lt;/td&gt;
&lt;td&gt;0.0046&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;ldquo;あなたの愛人の名前は&amp;rdquo;&lt;/td&gt;
&lt;td&gt;&amp;ldquo;島本 理生&amp;rdquo;&lt;/td&gt;
&lt;td&gt;0.0045&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;ldquo;婚活中毒&amp;rdquo;&lt;/td&gt;
&lt;td&gt;&amp;ldquo;秋吉 理香子&amp;rdquo;&lt;/td&gt;
&lt;td&gt;0.0044&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;ldquo;きまぐれな夜食カフェ - マカン・マラン みたび (単行本)&amp;rdquo;&lt;/td&gt;
&lt;td&gt;&amp;ldquo;古内 一絵&amp;rdquo;&lt;/td&gt;
&lt;td&gt;0.0043&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;ldquo;BUTTER&amp;rdquo;&lt;/td&gt;
&lt;td&gt;&amp;ldquo;柚木 麻子&amp;rdquo;&lt;/td&gt;
&lt;td&gt;0.0043&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;ldquo;デートクレンジング&amp;rdquo;&lt;/td&gt;
&lt;td&gt;&amp;ldquo;柚木 麻子&amp;rdquo;&lt;/td&gt;
&lt;td&gt;0.0043&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p&gt;この辺りの本をよく読む方なら納得ではないでしょうか。&lt;/p&gt;
&lt;p&gt;「&lt;a href="https://www.amazon.co.jp/dp/4344429664" target="_blank" rel="noopener noreferrer"&gt;わたしたちは銀のフォークと薬を手にして&lt;/a&gt;」と「&lt;a href="https://www.amazon.co.jp/dp/4087442349" target="_blank" rel="noopener noreferrer"&gt;大人は泣かないと思っていた&lt;/a&gt;」はわたしのAmazonのほしいものリストに入っていました。自分で作ったレコメンドエンジンに好みを当てられていますね。&lt;/p&gt;
&lt;p&gt;「&lt;a href="https://www.amazon.co.jp/dp/4396635419" target="_blank" rel="noopener noreferrer"&gt;デートクレンジング&lt;/a&gt;」が気になったので実際に買って読んでみました。文庫本では「&lt;a href="https://www.amazon.co.jp/dp/457552459X" target="_blank" rel="noopener noreferrer"&gt;踊る彼女のシルエット&lt;/a&gt;」に改題されています。女性はある程度の年齢になると結婚や出産の有無で規定されがちという息苦しさをテーマにした小説です。ストーリーの展開は好みが分かれそうですが、柚木さんらしい視線のするどさもあってお気に入りの一冊になりました。これは「大人になったら、」と近いテーマでして、レコメンドエンジンの真骨頂を感じました。&lt;/p&gt;
&lt;p&gt;レコメンドエンジンのすごさを感じたのはこちらの二冊です。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;青山美智子「&lt;a href="https://www.amazon.co.jp/dp/4800297125" target="_blank" rel="noopener noreferrer"&gt;木曜日にはココアを&lt;/a&gt;」&lt;/li&gt;
&lt;li&gt;長月天音「&lt;a href="https://www.amazon.co.jp/dp/4041139872" target="_blank" rel="noopener noreferrer"&gt;キッチン常夜灯&lt;/a&gt;」&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;後述のとおり課題点なのですが、実行するとこの二人の本ばかり上位に出てしまうので、結果のうち二人の本以外のものに絞って載せます。&lt;/p&gt;
&lt;div&gt;&lt;style&gt;
.dataframe &gt; thead &gt; tr,
.dataframe &gt; tbody &gt; tr {
text-align: right;
white-space: pre-wrap;
}
&lt;/style&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;title&lt;/th&gt;
&lt;th&gt;author&lt;/th&gt;
&lt;th&gt;score&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&amp;ldquo;三千円の使いかた (中公文庫 は 74-1)&amp;rdquo;&lt;/td&gt;
&lt;td&gt;&amp;ldquo;原田 ひ香&amp;rdquo;&lt;/td&gt;
&lt;td&gt;0.0355&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;ldquo;傲慢と善良 (朝日文庫)&amp;rdquo;&lt;/td&gt;
&lt;td&gt;&amp;ldquo;辻村 深月&amp;rdquo;&lt;/td&gt;
&lt;td&gt;0.0344&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;ldquo;満月珈琲店の星詠み (文春文庫 も 29-21)&amp;rdquo;&lt;/td&gt;
&lt;td&gt;&amp;ldquo;望月 麻衣&amp;rdquo;&lt;/td&gt;
&lt;td&gt;0.0338&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;ldquo;和菓子のアン (光文社文庫 さ 24-3)&amp;rdquo;&lt;/td&gt;
&lt;td&gt;&amp;ldquo;坂木 司&amp;rdquo;&lt;/td&gt;
&lt;td&gt;0.0296&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;ldquo;マカン・マラン - 二十三時の夜食カフェ&amp;rdquo;&lt;/td&gt;
&lt;td&gt;&amp;ldquo;古内 一絵&amp;rdquo;&lt;/td&gt;
&lt;td&gt;0.0295&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;ldquo;52ヘルツのクジラたち (単行本)&amp;rdquo;&lt;/td&gt;
&lt;td&gt;&amp;ldquo;町田 そのこ&amp;rdquo;&lt;/td&gt;
&lt;td&gt;0.0274&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;ldquo;タルト・タタンの夢 (創元推理文庫)&amp;rdquo;&lt;/td&gt;
&lt;td&gt;&amp;ldquo;近藤 史恵&amp;rdquo;&lt;/td&gt;
&lt;td&gt;0.0268&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;ldquo;コンビニ人間 (文春文庫 む 16-1)&amp;rdquo;&lt;/td&gt;
&lt;td&gt;&amp;ldquo;村田 沙耶香&amp;rdquo;&lt;/td&gt;
&lt;td&gt;0.0255&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;ldquo;夜空に泳ぐチョコレートグラミー (新潮文庫)&amp;rdquo;&lt;/td&gt;
&lt;td&gt;&amp;ldquo;町田 そのこ&amp;rdquo;&lt;/td&gt;
&lt;td&gt;0.0253&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;ldquo;そして、バトンは渡された (文春文庫 せ 8-3)&amp;rdquo;&lt;/td&gt;
&lt;td&gt;&amp;ldquo;瀬尾 まいこ&amp;rdquo;&lt;/td&gt;
&lt;td&gt;0.0246&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p&gt;「木曜日にはココアを」はカフェ、「キッチン常夜灯」はビストロを舞台に、そこに集う人たちの悩みを癒やしていく物語です。&lt;/p&gt;
&lt;p&gt;「&lt;a href="https://www.amazon.co.jp/dp/416791400X" target="_blank" rel="noopener noreferrer"&gt;満月珈琲店の星詠み&lt;/a&gt;」、「&lt;a href="https://www.amazon.co.jp/dp/4334764843" target="_blank" rel="noopener noreferrer"&gt;和菓子のアン&lt;/a&gt;」、「&lt;a href="https://www.amazon.co.jp/dp/4120047881" target="_blank" rel="noopener noreferrer"&gt;マカン・マラン -
二十三時の夜食カフェ&lt;/a&gt;」、「&lt;a href="https://www.amazon.co.jp/dp/4488427049" target="_blank" rel="noopener noreferrer"&gt;タルト・タタンの夢&lt;/a&gt;」と、飲食店が舞台で似たテーマの小説が出てくるのは素晴らしいですね。特徴量は各ユーザがそれぞれの本を読んだか読んでいないかというデータであり、あらすじの情報は用いていないにもかかわらず、好みに合いそうな小説を上手に選ぶことができています。&lt;/p&gt;
&lt;h2 id="おわりに"&gt;おわりに&lt;/h2&gt;
&lt;p&gt;想像していたよりもいい感じにレコメンドできたので、StreamlitでWebアプリにして早速使いつつ（このアプリは個人利用目的であり非公開です）、レコメンドされた作品をいくつか買って読んでいます。&lt;/p&gt;
&lt;p&gt;今のモデルでは、インプットに入れた本の作家の別の本ばかりがレコメンドされたりする課題があり、改善したいところです。同じ作家の本を読む人が多いから協調フィルタリングがそのような結果を返すのは当然なのですが、「推薦システム実践入門」にも書いてあるように、レコメンド結果の面白さは「意外性」が大切です。&lt;/p&gt;
&lt;p&gt;iALSは学習が高速ながら精度が出る優れたモデルなので、先に挙げた論文を読んで勉強してハイパーパラメータチューニングにも取り組みたいです。&lt;/p&gt;
&lt;h2 id="参考文献"&gt;参考文献&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.oreilly.co.jp/books/9784873119663/" target="_blank" rel="noopener noreferrer"&gt;推薦システム実践入門&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://recruit.gmo.jp/engineer/jisedai/blog/sketchfab_implicit_feedback/" target="_blank" rel="noopener noreferrer"&gt;ユーザーの暗黙的フィードバック(Implicit
Feedback)からオススメアイテムを推奨したい - GMOインターネットグループ
グループ研究開発本部&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://recruit.gmo.jp/engineer/jisedai/blog/revisiting-ials/" target="_blank" rel="noopener noreferrer"&gt;Revisiting iALS
〜最新の機械学習モデルにも匹敵する定番の推薦アルゴリズムの真の性能〜 -
GMOインターネットグループ
グループ研究開発本部&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developers.cyberagent.co.jp/blog/archives/38632/" target="_blank" rel="noopener noreferrer"&gt;推薦システムにおいて線形モデルがまだまだ有用な話 | CyberAgent
Developers
Blog&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://engineering.visional.inc/blog/393/ials-revisited/" target="_blank" rel="noopener noreferrer"&gt;iALSによる行列分解の知られざる真の実力 - Visional Engineering
Blog&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://qiita.com/yuwewe/items/ae8dc816f2531df404ff" target="_blank" rel="noopener noreferrer"&gt;iALSの論文実装と評価〜推薦システムの手法〜 #Python -
Qiita&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.nogawanogawa.com/entry/ials" target="_blank" rel="noopener noreferrer"&gt;iALSを使ってみる -
Re:ゼロから始めるML生活&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>t分布を用いたロバストな家賃相場の階層ベイズモデリング</title><link>https://suzunano.net/posts/robust-rent-modeling/</link><pubDate>Sun, 16 Mar 2025 00:00:00 +0900</pubDate><guid>https://suzunano.net/posts/robust-rent-modeling/</guid><description>&lt;h2 id="はじめに"&gt;はじめに&lt;/h2&gt;
&lt;p&gt;賃貸マンションの家賃相場は、おおむね、最寄り駅、面積、築年数、階数と、駅からの徒歩分数で決まります。これらの要因から家賃相場を推定する階層ベイズモデルを構築しました。なぜこんなことをしているのかというと、部屋探しの過程で家賃が決まるメカニズムに興味を持ったことがきっかけです。&lt;/p&gt;
&lt;p&gt;家賃データには誤入力や並外れた高額物件などによる外れ値が含まれます。誤差項に正規分布を仮定した通常のモデルでは、これらの外れ値に推定結果が引きずられてしまいます。そのためこの記事では、誤差項にt分布を用いることで、この問題に対処したロバストなモデルを作りました。&lt;/p&gt;
&lt;p&gt;2024年12月にSUUMOに掲載されていた東京23区の賃貸マンションの家賃データ（62万件）を用いて家賃相場を推定したところ、次の内容が分かりました。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;t分布の自由度は7.5程度
&lt;ul&gt;
&lt;li&gt;正規分布ではなく裾の重さを考慮することが妥当&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;家賃に与える影響
&lt;ul&gt;
&lt;li&gt;築年数1年あたり1.2%下がる&lt;/li&gt;
&lt;li&gt;徒歩1分あたり0.8%下がる&lt;/li&gt;
&lt;li&gt;階数が1階上がると0.9%上がる&lt;/li&gt;
&lt;li&gt;1階と地下1階は2階からみてそれぞれ4.6%、6.4%下がる&lt;/li&gt;
&lt;li&gt;最上階でも家賃相場は変わらない&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;t分布モデルでは、急行停車駅が各駅停車駅よりも家賃が高い現象や、都心に近い駅ほど家賃が高い現象をとらえることができた&lt;/li&gt;
&lt;li&gt;t分布モデルは正規分布モデルよりもWAICでの汎化性能が高かった&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="データ"&gt;データ&lt;/h2&gt;
&lt;p&gt;SUUMOから2024年12月にPythonでスクレイピングした、東京23区の賃貸マンションの家賃相場データ（約62万件）を用います。&lt;/p&gt;
&lt;p&gt;ただし、対象はマンションのみ、間取りは1K, 1DK, 1LDK, 2K, 2DK, 2LDK,
家賃+管理費が100万円以下、面積は20m2～100m2、築40年以内、最寄り駅徒歩20分以内、地上15階以下です&lt;sup id="fnref:1"&gt;&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref"&gt;1&lt;/a&gt;&lt;/sup&gt;。&lt;/p&gt;
&lt;p&gt;このようなデータフレームです。&lt;/p&gt;
&lt;img src="./features.png"&gt;
&lt;h2 id="外れ値の存在"&gt;外れ値の存在&lt;/h2&gt;
&lt;p&gt;x軸に面積の対数、y軸に家賃+管理費（以下家賃と呼びます）の対数をとり、築年数で色分けしてプロットしてみました。&lt;/p&gt;
&lt;img src="images/scatter-plot-1.png" style="width:80.0%" /&gt;
&lt;p&gt;面積の対数と家賃の対数はほぼ線形に並んでいるため、面積の対数と家賃の対数を線形回帰することは妥当だと思われますが、一部外れ値のような点があります。例えばexp(3.25)=25m2,
exp(4.5)=90万円は明らかな誤入力ですね（家賃を1桁多く入力しているようでした）。&lt;/p&gt;
&lt;p&gt;線形のライン状に密集しているエリアの少しだけ上と下にも点があります。これらは他の物件とは並外れた特徴がある物件かもしれません。このような物件が一定数あるということは、裾が正規分布より厚い分布に従っているということです。&lt;/p&gt;
&lt;p&gt;線形回帰の誤差項を正規分布より裾が厚いt分布やコーシー分布とすることで、どちらのケースにも対応して外れ値に引っ張られないロバストな回帰を行うことができます。こちらの記事が分かりやすいです。&lt;/p&gt;
&lt;p&gt;&lt;a href="https://suzuzusu.hatenablog.com/entry/2019/11/17/153000" target="_blank" rel="noopener noreferrer"&gt;尤度関数におけるガウス分布とスチューデントのt分布の比較 -
suzuzusu日記&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;散布図を見る限りはそこまで裾が重そうな分布ではないのでコーシー分布とするとやりすぎかと思ったのと、自由度が1のt分布はコーシー分布でありt分布はコーシー分布を包含するため、この記事ではt分布を適用してみます。&lt;/p&gt;
&lt;h2 id="モデル"&gt;モデル&lt;/h2&gt;
&lt;p&gt;家賃相場は最寄り駅、面積、築年数、最寄り駅からの徒歩分数、階数で決まるとします。これはEDAから妥当な仮定であることが分かっているのと、部屋の方角などの他の特徴量を取得することはスクレイピングの時間的制約から難しいため、これらの特徴量だけを用います。ちなみに、このように家賃などの不動産価格を面積などの属性の関数として表すアプローチをヘドニック法といいます（ヘドニック法については清水・唐渡
(2007) が分かりやすかったです）。&lt;/p&gt;
&lt;p&gt;最寄り駅によって家賃の水準と面積に対する家賃の弾力性が異なることを階層ベイズで表現します。これにより、物件数が少ない駅でも、東京23区の全体の平均の傾向を借用できる（「縮約」という）ことからパラメータの推定が安定します。&lt;/p&gt;
&lt;p&gt;築年数と徒歩分数、階数の影響は共通とします。駅近の方が築古でも家賃が下がりにくいと一般に言われますが、最も効くのは最寄り駅別の家賃水準の違いなので、こちらの影響は共通とします。もちろん高度化の余地はあります。&lt;/p&gt;
&lt;p&gt;散布図で見たとおり、家賃と面積は両対数線形の関係とします。また、築年数、徒歩分数、階数の効果は家賃に対して乗算で効くというドメイン的に自然な仮定を置きます（例えば、築1年増えるごとに1%下がるようなイメージです）。これによって、対数を取ると線形モデルとなり、扱いやすくなります。線形モデルの誤差項はt分布とすることで裾の厚さを表現します。&lt;/p&gt;
&lt;p&gt;これらをモデルにすると、以下のとおりとなります。&lt;/p&gt;
&lt;p&gt;物件$i(1, \dots, N)$の最寄り駅を$sta[i] (1, \dots, S)$とします。&lt;/p&gt;
&lt;p&gt;$$
\begin{align*}
\log{y_{i}} &amp;amp; \sim student\_t(\nu, \mu_{i}, \sigma) \\\
\mu_{i} &amp;amp;= a_{sta[i]} + b_{sta[i]} \log{\mathrm{area}_{i}} \\\
&amp;amp;+ \beta_{\mathrm{age}} \mathrm{age}_{i} \\\
&amp;amp;+ \beta_{\mathrm{walk}}(\mathrm{walk}_{i} - 1) \\\
&amp;amp;+ \beta_{\mathrm{floor}} \max {(\mathrm{floor}_{i} - 2, 0)} \\\
&amp;amp;+ \beta_{\mathrm{isTop}} \mathrm{isTop}_{i} \\\
&amp;amp;+ \beta_{\mathrm{isGround}} \mathrm{isGround}_{i} \\\
&amp;amp;+ \beta_{\mathrm{isUnderground}} \mathrm{isUnderground}_{i} \\\
a_{sta[i]} &amp;amp; \sim N(a_{all}, \sigma_{a_{all}}) \\\
b_{sta[i]} &amp;amp; \sim N(b_{all}, \sigma_{b_{all}}) \\\
\end{align*}
$$&lt;/p&gt;
&lt;p&gt;このとき、物件の対数家賃の相場は$\mu_{i}$万円であると考えます。ただし、式中の変数は以下のとおりです。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$y_{i}$: 家賃+管理費（万円）&lt;/li&gt;
&lt;li&gt;$\mathrm{area}_{i} (20 \leq \mathrm{area}_{i} \leq 100)$: 面積（m2）&lt;/li&gt;
&lt;li&gt;$\mathrm{age}_{i} (= 0, 1, \dots, 40)$: 築年数（新築は0年とする）&lt;/li&gt;
&lt;li&gt;$\mathrm{walk}_{i} (= 1, 2, \dots, 20)$: 最寄り駅からの徒歩分数&lt;/li&gt;
&lt;li&gt;$\mathrm{floor}_{i} (= -1, 1, 2, \dots, 15)$: 物件の階数&lt;/li&gt;
&lt;li&gt;$\mathrm{isTop}_{i} (= 0, 1)$: 最上階なら1, そうではないなら0&lt;/li&gt;
&lt;li&gt;$\mathrm{isGround}_{i} (= 0, 1)$: 1階なら1, そうではないなら0&lt;/li&gt;
&lt;li&gt;$\mathrm{isUnderground}_{i} (= 0, 1)$: 地下1階なら1,
そうではないなら0&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;式中の$\nu$はt分布の自由度です。&lt;a href="https://www.amazon.co.jp/dp/4320112423" target="_blank" rel="noopener noreferrer"&gt;StanとRでベイズ統計モデリング&lt;/a&gt;によれば、そこまで裾が重くない分布の場合は自由度6～8を設定するとよいそうです。なので自由度を6～8くらいの定数と決め打ちしてもいいと思いますが、自由度もパラメータとして推定することにします。&lt;/p&gt;
&lt;p&gt;なお、モデル比較用に別途、t分布のところを正規分布としたモデルも推定しました。&lt;/p&gt;
&lt;h2 id="実装"&gt;実装&lt;/h2&gt;
&lt;p&gt;先のモデルを以下のStanコードで実装しました。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-stan"&gt;data {
int N; // 物件の数
vector[N] Y; // 家賃+管理費
vector[N] AREA; // 面積
int S; // 最寄り駅の数
array[N] int&amp;lt;lower=1, upper=S&amp;gt; STATION; // 最寄り駅index
vector[N] AGE; // 築年数（0 - 40）
vector[N] WALK; // 徒歩分数（1 - 20）
vector[N] FLOOR; // 階数（-1, 1 - 15）
vector[N] IS_TOP; // 最上階かどうか（0/1）
vector[N] IS_GROUND; // 1階かどうか（0/1）
vector[N] IS_UNDERGROUND; // 地下1階かどうか（0/1）
}
transformed data {
vector[N] FLOOR2;
for (i in 1:N) {
if (FLOOR[i] &amp;lt;= 1) {
FLOOR2[i] = 2;
} else {
FLOOR2[i] = FLOOR[i];
}
}
}
parameters {
real a_all;
real b_all;
vector[S] a;
vector[S] b;
real&amp;lt;upper=0&amp;gt; age;
real&amp;lt;upper=0&amp;gt; walk;
real&amp;lt;lower=0&amp;gt; floor_num;
real&amp;lt;lower=0&amp;gt; is_top;
real&amp;lt;upper=0&amp;gt; is_ground;
real&amp;lt;upper=0&amp;gt; is_underground;
real&amp;lt;lower=0&amp;gt; sigma_a;
real&amp;lt;lower=0&amp;gt; sigma_b;
real&amp;lt;lower=0&amp;gt; sigma;
real&amp;lt;lower=1&amp;gt; nu;
}
model {
a ~ normal(a_all, sigma_a);
b ~ normal(b_all, sigma_b);
log(Y) ~ student_t(
nu,
a[STATION] + b[STATION] .* log(AREA) +
age*AGE +
walk*(WALK - 1)+
floor_num*(FLOOR2 - 2) +
is_top*IS_TOP +
is_ground*IS_GROUND +
is_underground*IS_UNDERGROUND,
sigma
);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;cmdstanrの&lt;code&gt;cmdstan_model&lt;/code&gt;の&lt;code&gt;sample&lt;/code&gt;を用いて、chains=4, iter_warmup=1000,
iter_sampling=1000,
thin=1でサンプリングしました。4chain並列で4日かかりました。環境はR=4.4.2,
CmdStan=2.35.0, cmdstanr=0.8.1です。&lt;/p&gt;
&lt;h2 id="結果"&gt;結果&lt;/h2&gt;
&lt;h3 id="パラメータ"&gt;パラメータ&lt;/h3&gt;
&lt;pre&gt;&lt;code class="language-r"&gt;fit$print(
c(&amp;quot;age&amp;quot;, &amp;quot;walk&amp;quot;, &amp;quot;floor_num&amp;quot;, &amp;quot;is_top&amp;quot;, &amp;quot;is_ground&amp;quot;, &amp;quot;is_underground&amp;quot;, &amp;quot;sigma&amp;quot;, &amp;quot;nu&amp;quot;),
max_rows=12, digits=3
)
#&amp;gt; variable mean median sd mad q5 q95 rhat ess_bulk ess_tail
#&amp;gt; age -0.012 -0.012 0.000 0.000 -0.012 -0.012 1.001 3951 2624
#&amp;gt; walk -0.008 -0.008 0.000 0.000 -0.008 -0.008 1.001 7615 3078
#&amp;gt; floor_num 0.009 0.009 0.000 0.000 0.009 0.009 1.000 6234 2861
#&amp;gt; is_top 0.000 0.000 0.000 0.000 0.000 0.000 1.001 3665 2064
#&amp;gt; is_ground -0.049 -0.049 0.000 0.000 -0.049 -0.048 1.000 7561 2916
#&amp;gt; is_underground -0.066 -0.066 0.002 0.002 -0.070 -0.062 1.002 7136 2928
#&amp;gt; sigma 0.093 0.093 0.000 0.000 0.093 0.094 1.000 6458 3222
#&amp;gt; nu 7.562 7.561 0.076 0.074 7.440 7.689 1.000 7036 3119
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;自由度$\nu$の推定値は7.56でした。正規分布とはいえないくらいには裾が重い分布ということを示します。&lt;/p&gt;
&lt;p&gt;築年数効果（&lt;code&gt;age&lt;/code&gt;）、徒歩分数効果（&lt;code&gt;walk&lt;/code&gt;）、階数効果（&lt;code&gt;floor_num&lt;/code&gt;）、最上階効果（&lt;code&gt;is_top&lt;/code&gt;）、1階効果（&lt;code&gt;is_ground&lt;/code&gt;）、地下1階効果（&lt;code&gt;is_underground&lt;/code&gt;）の各パラメータより、以下のことが分かりました&lt;sup id="fnref:2"&gt;&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref"&gt;2&lt;/a&gt;&lt;/sup&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;築年数が1年増えるごとに家賃相場は1.2%下がる&lt;/li&gt;
&lt;li&gt;徒歩1分増えるごとに0.8%下がる&lt;/li&gt;
&lt;li&gt;2階から上に1階高くなるごとに0.9%上がる&lt;/li&gt;
&lt;li&gt;1階と地下1階は2階から見てそれぞれ4.6%、6.4%下がる&lt;/li&gt;
&lt;li&gt;最上階でも家賃相場は変わらない&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;これらは比較用の正規分布モデルと大きく変わりませんでした。&lt;/p&gt;
&lt;p&gt;階数のところですが、例えば4階建てのマンションで2階が家賃10万円なら、3階は10.09万円、4階は10.18万円、1階は9.54万円であり、直感的な結果なのではないでしょうか。&lt;/p&gt;
&lt;h3 id="最寄り駅ごとの家賃相場"&gt;最寄り駅ごとの家賃相場&lt;/h3&gt;
&lt;p&gt;正規分布ではなくt分布を設定したことで、最寄り駅ごとの家賃相場をドメイン知識に合った形でうまく推定することができました。その一例を挙げます。&lt;/p&gt;
&lt;p&gt;25m2、新築、駅から徒歩5分、3階の物件を仮定して、小田急線の最寄り駅別の家賃相場を求めてみます&lt;sup id="fnref:3"&gt;&lt;a href="#fn:3" class="footnote-ref" role="doc-noteref"&gt;3&lt;/a&gt;&lt;/sup&gt;。&lt;/p&gt;
&lt;img src="images/odakyu-1.png" style="width:80.0%" /&gt;
&lt;p&gt;左のプロットが今回推定したt分布のモデル、右のプロットは比較用に別途推定した正規分布のモデルです。横軸の単位は万円で、黒い点は事後分布の中央値、横の棒は95%信用区間です。&lt;/p&gt;
&lt;p&gt;今回のt分布のモデルの方がうまく推定できたと思われるところは二つあります。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;成城学園前と祖師ヶ谷大蔵
&lt;ul&gt;
&lt;li&gt;成城学園前は急行が止まり、祖師ヶ谷大蔵は各駅のみの停車駅&lt;/li&gt;
&lt;li&gt;正規分布のモデルではほぼ差がないが、t分布モデルでは成城学園前＞祖師ヶ谷大蔵&lt;/li&gt;
&lt;li&gt;祖師ヶ谷大蔵は成城学園前より一駅分都心に近いが、成城学園前は急行停車駅であることに加えて高級住宅街であるため、t分布の方の結果が納得できる&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;梅ヶ丘と豪徳寺、千歳船橋と祖師ヶ谷大蔵
&lt;ul&gt;
&lt;li&gt;4駅とも各駅停車駅&lt;/li&gt;
&lt;li&gt;正規分布モデルでは梅ヶ丘＜豪徳寺、千歳船橋＜祖師ヶ谷大蔵。t分布モデルでは、95%有意ではないものの、梅ヶ丘＞豪徳寺、千歳船橋＞祖師ヶ谷大蔵&lt;/li&gt;
&lt;li&gt;都心に近い方が家賃が高くなるt分布モデルの方がドメイン知識に整合的&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="waicによる汎化誤差の比較"&gt;WAICによる汎化誤差の比較&lt;/h3&gt;
&lt;p&gt;このようなきれいなストーリーが偶然ではないことを知りたいですね。汎化誤差が小さいモデルを選ぶために、WAICを計算してみます。&lt;/p&gt;
&lt;p&gt;WAICを算出するには、まずStanのコードの&lt;code&gt;model&lt;/code&gt;ブロックの下に以下の&lt;code&gt;generated quantities&lt;/code&gt;ブロックを追加して対数尤度を計算しておきます。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-stan"&gt;generated quantities {
vector[N] log_lik; // 対数尤度
for (i in 1:N) {
log_lik[i] = student_t_lpdf(
log(Y[i]) |
nu,
a[STATION[i]] + b[STATION[i]] .* log(AREA[i]) +
age*AGE[i] +
walk*(WALK[i] - 1)+
floor_num*(FLOOR2[i] - 2) +
is_top*IS_TOP[i] +
is_ground*IS_GROUND[i] +
is_underground*IS_UNDERGROUND[i],
sigma
);
}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;これにより&lt;code&gt;log_lik&lt;/code&gt;で対数尤度を持っている&lt;code&gt;CmdStanFit&lt;/code&gt;モデルを作っておけば、&lt;code&gt;loo::waic()&lt;/code&gt;でWAICが計算できます。&lt;/p&gt;
&lt;p&gt;今回使ったデータのうち、渋谷区の物件のデータ（約41000件）を用いてパラメータを推定してWAICを求めたところ、以下のとおりでした。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;t分布（自由度もパラメータとして推定）: -74652.41&lt;/li&gt;
&lt;li&gt;正規分布: -73071.94&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;このことからも正規分布よりはt分布の方が汎化性能がよいことが分かります。&lt;/p&gt;
&lt;h2 id="おわりに"&gt;おわりに&lt;/h2&gt;
&lt;p&gt;t分布の導入により、外れ値にロバストなモデルを作ることができました。現実のデータは裾が厚い分布に従うことがあるのでt分布は役立ちますね。&lt;/p&gt;
&lt;h2 id="参考文献"&gt;参考文献&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;清水千弘・唐渡広志 (2007).『不動産市場の計量経済分析』朝倉書店.&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="footnotes" role="doc-endnotes"&gt;
&lt;hr&gt;
&lt;ol&gt;
&lt;li id="fn:1"&gt;
&lt;p&gt;面積、築年数、最寄り駅、階数の条件は、物件数が一定程度存在する領域に絞ったという理由です。特に階数ですが、地上15階を超える物件は少ないです。建築基準法・消防法上、15階建て程度までであれば非常用エレベータやスプリンクラーの設置が免除されることから、コスト的に15階建てを超えるマンションは立てにくいものだと思われます。間取りですが、ワンルームは部屋の設備が簡略化されていることから、3LDK以上は物件数が少ないうえに近年の分譲マンション価格の高騰でファミリー層が賃貸に流れていることから、その他の間取りとは相場特性が異なると考え、これらも除外しました。&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:2"&gt;
&lt;p&gt;各パラメータのmedianの$\exp$を取ったものです。&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:3"&gt;
&lt;p&gt;他に最上階ではないという条件も設定していますが、is_topが0であることから最上階かどうかは家賃に影響を与えないので、最上階だとしてもプロットは変わりません。&amp;#160;&lt;a href="#fnref:3" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</description></item><item><title>東京23区の賃貸マンションの家賃相場を階層ベイズで推定する（2024年12月版）</title><link>https://suzunano.net/posts/rent-modeling-update/</link><pubDate>Sat, 04 Jan 2025 00:00:00 +0900</pubDate><guid>https://suzunano.net/posts/rent-modeling-update/</guid><description>&lt;h2 id="はじめに"&gt;はじめに&lt;/h2&gt;
&lt;p&gt;階層ベイズモデルで東京23区の賃貸マンションの家賃相場を推定しました。&lt;/p&gt;
&lt;p&gt;2024年12月にSUUMOをスクレイピングして約60万件の東京23区の賃貸マンションの家賃データを取得しました。家賃相場の階層ベイズモデルをStanとR（cmdstanr）で実装してモデルのパラメータを推定することで、東京23区の最寄り駅別の家賃相場や、築年数、駅徒歩分数、階数による家賃の押し上げ・押し下げ効果を推定しました。&lt;/p&gt;
&lt;p&gt;最寄り駅ごとの家賃相場はSUUMOやHOME’Sなどの賃貸物件サイトで見ることができます。ただ、これらのサイトが公表している家賃相場は、何駅の1Kはいくら？くらいの粗い粒度です。面積が1m2違うだけでも数千円変わってきますから、例えば40m2で築5年で駅徒歩5分の物件はいくら？というような細かい家賃相場が知りたいですね。また、築10年の差はどの程度家賃相場が変わってくるのかも知りたいです。知りたいのですが、ここまで細かい家賃相場は知る限りネットに見つかりません。なければ自分で作る、というわけで実装しました。&lt;/p&gt;
&lt;p&gt;ちなみにこのブログでは以前にも家賃相場のベイズモデリングに関する記事を書いています。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="../rent-modeling/"&gt;階層ベイズで東京23区のお部屋の家賃相場を推定する&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="../rent-by-floor/"&gt;部屋の階数は家賃にどれだけ影響を与えるのか？&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;これらの記事でモデルは作れていたのですが、より安定した推定精度を得るためにデータ数を増やすなどいくつかのアップデートを行いました。&lt;/p&gt;
&lt;h2 id="データ"&gt;データ&lt;/h2&gt;
&lt;h3 id="データ取得と前処理"&gt;データ取得と前処理&lt;/h3&gt;
&lt;p&gt;SUUMOの東京23区の賃貸マンションの物件一覧ページを2024年12月に1日1回ずつ、計2週間ほどスクレイピングしました。Python（requests +
BeautifulSoup4）で実装しました。&lt;/p&gt;
&lt;p&gt;ある一時点のデータでは、スクレイピングしたときにたまたま高級物件の募集が多かった駅の家賃相場が高く推定されてしまいます。複数日のデータを用意することで、単純にデータ数を増やせるだけでなく、このような影響を軽減できます。&lt;/p&gt;
&lt;p&gt;複数日にわたって掲載されている物件は当然重複しますので、重複を除外します。それに加えてSUUMOでは同一の物件でも異なる物件として登録されていることがあるため、この重複も除外しました&lt;sup id="fnref:1"&gt;&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref"&gt;1&lt;/a&gt;&lt;/sup&gt;。&lt;/p&gt;
&lt;p&gt;そのうえで、分析やモデリングに使えるように前処理するとともに、データの誤入力や未入力と思われる物件を除外しました&lt;sup id="fnref:2"&gt;&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref"&gt;2&lt;/a&gt;&lt;/sup&gt;。&lt;/p&gt;
&lt;p&gt;ここまでで分析に使えるテーブルデータを作ることができました。以下のように、各行が個々の物件、列が特徴量のデータフレームです。87万件の物件データです。&lt;/p&gt;
&lt;img src="./features.png"&gt;
&lt;p&gt;同じマンションに複数の物件の募集が出ている場合は物件数だけレコードがあります。&lt;/p&gt;
&lt;p&gt;1行目の物件は、「千代田区麹町6丁目で家賃76万円、管理費5万円（家賃+管理費81万円）、敷金76万円、礼金0万円、3LDK、90m2、中央線四ツ谷駅から徒歩3分、築4年、12階（地上14階地下0階建て）」ということを意味します。81万円の賃貸ってすごいですね…。&lt;/p&gt;
&lt;p&gt;なお、最寄り駅はSUUMOには最大3駅まで書いてありますが、簡単のため最初の1駅の情報のみを利用します。また、物件の構造（鉄筋か鉄骨かなど）、部屋の方角、物件の設備（バストイレ別かや食洗器が付いているかなど）のようなより詳細なデータはありません&lt;sup id="fnref:3"&gt;&lt;a href="#fn:3" class="footnote-ref" role="doc-noteref"&gt;3&lt;/a&gt;&lt;/sup&gt;。&lt;/p&gt;
&lt;h3 id="使用するデータ"&gt;使用するデータ&lt;/h3&gt;
&lt;p&gt;取得できたデータのうち、以下を満たす物件のみを家賃相場の推定に使います。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;マンションのみ
&lt;ul&gt;
&lt;li&gt;理由:
アパートや一戸建ては家賃相場の推定上同一に扱えないため（築古による家賃押し下げ効果なども異なる）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;間取りは1K, 1DK, 1LDK, 2K, 2DK, 2LDK
&lt;ul&gt;
&lt;li&gt;理由:
1Rはバストイレ一緒の物件が多いなど1K以上と同一に扱えないため除外、3K以上も物件数が少ないので除外&lt;sup id="fnref:4"&gt;&lt;a href="#fn:4" class="footnote-ref" role="doc-noteref"&gt;4&lt;/a&gt;&lt;/sup&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;家賃+管理費100万円以下
&lt;ul&gt;
&lt;li&gt;理由: 外れ値を除くため&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;面積は20m2～100m2
&lt;ul&gt;
&lt;li&gt;理由: 間取りと同様&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;築40年まで
&lt;ul&gt;
&lt;li&gt;理由: 築40年を超える物件は少ないため除外&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;駅から徒歩20分以内
&lt;ul&gt;
&lt;li&gt;理由: 23区内では徒歩20分を超える物件は少ないため除外&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;マンションの階数（各物件の階数ではなく、建物自体の階数）は地上15階まで、かつ地下階はないか地下1階まで
&lt;ul&gt;
&lt;li&gt;理由:
16階建て以上の物件は少なく、タワーマンションのような高級物件となるので除外&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;ある程度均質なデータに絞るということです。外れ値の除外方法は工夫の余地があると思います。約62万件の物件データを用意できました。&lt;/p&gt;
&lt;h2 id="モデル"&gt;モデル&lt;/h2&gt;
&lt;p&gt;家賃相場は、物件の最寄り駅、面積、築年数、駅からの徒歩分数、部屋の階数で決まると考えます。実際の家賃は部屋の設備のようなその他の特徴量にも左右されますが、おおむねこれらで決まると考えても大きくは外さないでしょう。以下、家賃とは家賃+管理費を指します。&lt;/p&gt;
&lt;p&gt;物件$i(1, \dots, N)$の最寄り駅を$sta[i] (1, \dots, S)$とします。&lt;/p&gt;
&lt;p&gt;$$
\begin{align*}
\log{y_{i}} &amp;amp; \sim N(\mu_{i}, \sigma) \\\
\mu_{i} &amp;amp;= a_{sta[i]} + b_{sta[i]} \log{\mathrm{area}_{i}} \\\
&amp;amp;+ \beta_{\mathrm{age}} \mathrm{age}_{i} \\\
&amp;amp;+ \beta_{\mathrm{walk}}(\mathrm{walk}_{i} - 1) \\\
&amp;amp;+ \beta_{\mathrm{floor}} \max {(\mathrm{floor}_{i} - 2, 0)} \\\
&amp;amp;+ \beta_{\mathrm{isTop}} \mathrm{isTop}_{i} \\\
&amp;amp;+ \beta_{\mathrm{isGround}} \mathrm{isGround}_{i} \\\
&amp;amp;+ \beta_{\mathrm{isUnderground}} \mathrm{isUnderground}_{i} \\\
a_{sta[i]} &amp;amp; \sim N(a_{all}, \sigma_{a_{all}}) \\\
b_{sta[i]} &amp;amp; \sim N(b_{all}, \sigma_{b_{all}}) \\\
\end{align*}
$$&lt;/p&gt;
&lt;p&gt;このとき、物件の対数家賃の相場は$\mu_{i}$万円であると考えます。ただし、式中の変数は以下のとおりです。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$y_{i}$: 家賃+管理費（万円）&lt;/li&gt;
&lt;li&gt;$\mathrm{area}_{i} (20 \leq \mathrm{area}_{i} \leq 100)$: 面積（m2）&lt;/li&gt;
&lt;li&gt;$\mathrm{age}_{i} (= 0, 1, \dots, 40)$: 築年数（新築は0年とする）&lt;/li&gt;
&lt;li&gt;$\mathrm{walk}_{i} (= 1, 2, \dots, 20)$: 最寄り駅からの徒歩分数&lt;/li&gt;
&lt;li&gt;$\mathrm{floor}_{i} (= -1, 1, 2, \dots, 15)$: 物件の階数&lt;/li&gt;
&lt;li&gt;$\mathrm{isTop}_{i} (= 0, 1)$: 最上階なら1, そうではないなら0&lt;/li&gt;
&lt;li&gt;$\mathrm{isGround}_{i} (= 0, 1)$: 1階なら1, そうではないなら0&lt;/li&gt;
&lt;li&gt;$\mathrm{isUnderground}_{i} (= 0, 1)$: 地下1階なら1,
そうではないなら0&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;間取りの情報は入れていません。間取りと面積はかなり相関が強い変数であり多重共線性があるので面積のみをモデルに入れました。&lt;/p&gt;
&lt;p&gt;最寄り駅によって同じ面積でも家賃相場が違うことを考慮しています。築浅や駅近、高層階ほど家賃が高いことや、最上階は家賃が高いこと、1階や地下階は家賃が安いこともモデルに織り込んでいます。あまり物件が存在しない最寄り駅でも、全体の傾向を踏まえてパラメータを安定して推定できるのが階層ベイズのメリットです。&lt;/p&gt;
&lt;p&gt;ただし、このモデルでは以下のように単純化した定式化となっています。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;築年数は1年増えるごとに、駅徒歩1分増えるごとに家賃相場が一定割合減る&lt;/li&gt;
&lt;li&gt;1階高くなるごとに家賃相場が一定割合上がる&lt;/li&gt;
&lt;li&gt;築年数、駅徒歩、階数や、1階や地下1階であることがそれぞれ家賃相場を押し上げる・押し下げる効果は、全ての最寄り駅で一定であり、その他の変数とは独立&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;これらは一定程度強い仮定であることに注意が必要です。&lt;/p&gt;
&lt;p&gt;実際には、築浅物件と築古物件では、築1年経過することによる家賃の押し下げ効果は築浅物件の方が大きいと思われます。物件を検索するときは徒歩10分までのようなきりのよい値を指定することが多いため、徒歩10分と11分だと家賃相場が大きく変わるかもしれません。駅近ほどよいかというと、駅に近すぎると線路や駅周辺の騒音で家賃相場が安い可能性もあります。また、築古でも駅近や人気の駅、マンションの新規建設があまり行われていない地域では家賃が下がりにくいのも想像がつきます。&lt;/p&gt;
&lt;p&gt;モデルのブラッシュアップの余地はありますが、大まかな傾向をつかめればよいということでこのモデルを採用します。&lt;/p&gt;
&lt;h2 id="実装"&gt;実装&lt;/h2&gt;
&lt;p&gt;環境はR=4.4.2, cmdstan=2.35.0, cmdstanr=0.8.1, bayesplot=1.11.1,
tidybayes=3.0.7です。&lt;/p&gt;
&lt;p&gt;上のモデルをStanで書きます。このコードを”model.stan”というファイル名で保存します。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-stan"&gt;data {
int N;
vector[N] Y;
vector[N] AREA;
int S;
array[N] int&amp;lt;lower=1, upper=S&amp;gt; STATION; // 最寄り駅index
vector[N] AGE; // 築年数（0 - 40）
vector[N] WALK; // 徒歩分数（1 - 20）
vector[N] FLOOR; // 階数（2 - 15; -1と1は2とする）
vector[N] IS_TOP; // 最上階かどうか（0/1）
vector[N] IS_GROUND; // 1階かどうか（0/1）
vector[N] IS_UNDERGROUND; // 地下1階かどうか（0/1）
}
parameters {
real a_all;
real b_all;
vector[S] a;
vector[S] b;
real&amp;lt;upper=0&amp;gt; age;
real&amp;lt;upper=0&amp;gt; walk;
real&amp;lt;lower=0&amp;gt; floor_num;
real&amp;lt;lower=0&amp;gt; is_top;
real&amp;lt;upper=0&amp;gt; is_ground;
real&amp;lt;upper=0&amp;gt; is_underground;
real&amp;lt;lower=0&amp;gt; sigma_a;
real&amp;lt;lower=0&amp;gt; sigma_b;
real&amp;lt;lower=0&amp;gt; sigma;
}
model {
a ~ normal(a_all, sigma_a);
b ~ normal(b_all, sigma_b);
log(Y) ~ normal(
a[STATION] + b[STATION].*log(AREA) +
age*AGE +
walk*(WALK - 1)+
floor_num*(FLOOR - 2) +
is_top*IS_TOP +
is_ground*IS_GROUND +
is_underground*IS_UNDERGROUND,
sigma
);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;次に以下のRコードでstanコードをキックします。さきほど画像を載せたdata.frameを&lt;code&gt;df_unique&lt;/code&gt;という変数名で持っている前提です。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-r"&gt;library(tidyverse)
library(cmdstanr)
library(bayesplot)
library(tidybayes)
df &amp;lt;- df_unique |&amp;gt;
filter(rent_admin &amp;lt;= 100) |&amp;gt;
filter(area &amp;lt;= 100 &amp;amp; area &amp;gt;= 20) |&amp;gt;
filter(age &amp;lt;= 40) |&amp;gt;
filter(walk &amp;lt;= 20) |&amp;gt;
filter(story_under &amp;lt;= 1L &amp;amp; story_above &amp;lt;= 15L) |&amp;gt;
filter(floor &amp;gt;= -1L &amp;amp; floor &amp;lt;= 15L) |&amp;gt;
filter(layout %in% c(&amp;quot;1K&amp;quot;, &amp;quot;1DK&amp;quot;, &amp;quot;1LDK&amp;quot;, &amp;quot;2K&amp;quot;, &amp;quot;2DK&amp;quot;, &amp;quot;2LDK&amp;quot;)) |&amp;gt;
# Stanに入れるために、駅名をintegerに変換する
mutate(station_index=as.integer(as.factor(station))) |&amp;gt;
mutate(
# 平屋や2階建ての2階の場合は「最上階」とはみなさないことにする（その方が直感的に自然なので）
is_top=as.integer(floor == story_above &amp;amp; story_above &amp;gt;= 3),
is_ground=as.integer(floor == 1L),
is_underground=as.integer(floor &amp;lt;= -1L)
)
mod &amp;lt;- cmdstanr::cmdstan_model(&amp;quot;model.stan&amp;quot;)
fit &amp;lt;- mod$sample(
data=list(
N=nrow(df),
Y=df$rent_admin,
AREA=df$area,
S=length(unique(df$station_index)),
STATION=df$station_index,
AGE=df$age,
WALK=df$walk,
# 地下1階, 1階は&amp;quot;2&amp;quot;に変換して入れる
FLOOR=df |&amp;gt;
mutate(floor2=if_else(floor &amp;lt;= 1L, 2L, floor)) |&amp;gt;
pull(floor2),
IS_TOP=df$is_top,
IS_GROUND=df$is_ground,
IS_UNDERGROUND=df$is_underground
),
chains=4, parallel_chains=4, iter_warmup=1000, iter_sampling=1000, thin=1,
seed=1234, refresh=10
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;StanコードをRからキックするパッケージは、前の記事まではrstanを用いていましたが、cmdstanrに乗り換えました。コンパイルが早い、動作が安定していてクラッシュしにくい、開発が盛ん、OpenCLでGPUも使えるなどいいことづくめです。&lt;/p&gt;
&lt;p&gt;warmupを入れて合計2000回のiterationで約2日かかりました。&lt;/p&gt;
&lt;h2 id="結果"&gt;結果&lt;/h2&gt;
&lt;h3 id="パラメータ"&gt;パラメータ&lt;/h3&gt;
&lt;p&gt;Rhat &amp;lt;
1.1であること以外にもStanによるMCMCの収束チェックは行いましたが、記事上は省略します&lt;sup id="fnref:5"&gt;&lt;a href="#fn:5" class="footnote-ref" role="doc-noteref"&gt;5&lt;/a&gt;&lt;/sup&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-r"&gt;fit$print(
c(&amp;quot;a_all&amp;quot;, &amp;quot;b_all&amp;quot;, &amp;quot;age&amp;quot;, &amp;quot;walk&amp;quot;, &amp;quot;floor_num&amp;quot;, &amp;quot;is_top&amp;quot;, &amp;quot;is_ground&amp;quot;, &amp;quot;is_underground&amp;quot;,
&amp;quot;sigma_a&amp;quot;, &amp;quot;sigma_b&amp;quot;, &amp;quot;sigma&amp;quot;),
max_rows=11, digits=3
)
#&amp;gt; variable mean median sd mad q5 q95 rhat ess_bulk ess_tail
#&amp;gt; a_all -0.146 -0.146 0.016 0.016 -0.172 -0.120 1.001 7784 2950
#&amp;gt; b_all 0.839 0.839 0.006 0.006 0.829 0.848 1.001 7788 2648
#&amp;gt; age -0.012 -0.012 0.000 0.000 -0.012 -0.012 1.000 4176 2826
#&amp;gt; walk -0.008 -0.008 0.000 0.000 -0.008 -0.008 0.999 10482 2819
#&amp;gt; floor_num 0.009 0.009 0.000 0.000 0.009 0.009 1.002 9145 3069
#&amp;gt; is_top 0.000 0.000 0.000 0.000 0.000 0.000 1.001 5474 2358
#&amp;gt; is_ground -0.047 -0.047 0.000 0.000 -0.048 -0.046 1.000 8558 3008
#&amp;gt; is_underground -0.069 -0.069 0.002 0.002 -0.073 -0.065 1.004 10218 2805
#&amp;gt; sigma_a 0.333 0.333 0.012 0.012 0.315 0.353 1.000 5732 2706
#&amp;gt; sigma_b 0.119 0.119 0.004 0.004 0.113 0.126 1.000 7172 3115
#&amp;gt; sigma 0.109 0.109 0.000 0.000 0.109 0.109 1.000 3873 2026
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id="築年数効果駅徒歩分数効果階数効果最上階1階地下1階効果"&gt;築年数効果、駅徒歩分数効果、階数効果、最上階・1階・地下1階効果&lt;/h3&gt;
&lt;p&gt;パラメータ&lt;code&gt;age&lt;/code&gt;は事後分布の中央値が-0.0119でした。築年数が1年増えるごとに家賃相場の対数$\mu_{i}$が-0.0119小さくなることを意味します。つまり、築年数が1年増えるごとに家賃相場$\exp(\mu_{i})$は$\exp(-0.0119) = 0.988$倍になる、すなわち&lt;strong&gt;築年数が1年増えるごとに家賃相場は1.2%下がる&lt;/strong&gt;ということです。新築と比べると築5年は約6%、築10年は約11%家賃が下がることになります。&lt;/p&gt;
&lt;p&gt;&lt;a href="https://www.smtri.jp/report_column/report/2013_01_16_1521.html" target="_blank" rel="noopener noreferrer"&gt;築1年経過するごとに家賃が1%下がるという経験則&lt;/a&gt;があるそうです。結果はこの経験則と整合的ですね。ただし今引用したレポートでは、築10年までの築浅物件とそれ以降の物件では前者の方が経年による家賃の下落率が高いと指摘されています。この点の考慮は今後の課題です。&lt;/p&gt;
&lt;p&gt;同様に&lt;strong&gt;駅徒歩1分増えるごとに家賃相場は0.8%下がる&lt;/strong&gt;ことが分かりました。&lt;/p&gt;
&lt;p&gt;また、&lt;strong&gt;2階から見て1階上がるごとに家賃相場は0.9%上がります。また、1階と地下1階は2階から見てそれぞれ4.6%、6.7%家賃相場が下がります。&lt;/strong&gt;&lt;code&gt;is_underground&lt;/code&gt;のパラメータの95%信用区間の下限が0を上回らないことから、&lt;strong&gt;最上階であっても家賃は変わらない&lt;/strong&gt;と言えることも分かりました。最上階はお得ですね！&lt;/p&gt;
&lt;p&gt;例えば地上4階地下1階建てのマンションで2階が家賃10万円なら、3階は10.09万円、4階は10.18万円、1階は9.54万円、地下1階は9.33万円程度になる計算になります。だいぶ妥当な感じがします。1階は避けたがる人も多いですが、5%安いメリットを天秤にかけてどう判断するかですね。&lt;/p&gt;
&lt;h3 id="最寄り駅ごとの家賃相場"&gt;最寄り駅ごとの家賃相場&lt;/h3&gt;
&lt;p&gt;25m2、新築、駅から徒歩5分、3階の物件を仮定して、この物件の最寄り駅別の家賃相場を求めてみましょう&lt;sup id="fnref:6"&gt;&lt;a href="#fn:6" class="footnote-ref" role="doc-noteref"&gt;6&lt;/a&gt;&lt;/sup&gt;。25m2というのは1Kや1DKでよくある面積です。特に1Kでは25m2～26m2というサイズの物件が非常に多いです。MCMCで得られた各パラメータのサンプリングされた値を用いて$\exp(\mu_{i})$の分布を求めることで計算できます。&lt;/p&gt;
&lt;p&gt;JR中央線の新宿より西側を見てみます。&lt;/p&gt;
&lt;details class="code-fold"&gt;
&lt;summary&gt;Code&lt;/summary&gt;
&lt;pre&gt;&lt;code class="language-r"&gt;draws &amp;lt;- tidybayes::spread_draws(
fit, a[station_index], b[station_index], age, walk, floor_num, is_top, is_ground, is_underground
)
station_index_table &amp;lt;- df |&amp;gt;
select(station, station_index) |&amp;gt;
distinct(station, .keep_all=TRUE)
# 駅名があればそのindex, なければNA_integer_を返す
station_to_idx &amp;lt;- function(station_name) {
chr &amp;lt;- station_index_table$station
idx &amp;lt;- station_index_table$station_index
if (length(idx[which(chr==station_name)]) == 0) {
return(NA_integer_)
} else {
return(idx[which(chr==station_name)])
}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/details&gt;
&lt;details class="code-fold"&gt;
&lt;summary&gt;Code&lt;/summary&gt;
&lt;pre&gt;&lt;code class="language-r"&gt;stations &amp;lt;- c(
&amp;quot;新宿駅&amp;quot;, &amp;quot;大久保駅&amp;quot;, &amp;quot;東中野駅&amp;quot;, &amp;quot;中野駅&amp;quot;, &amp;quot;高円寺駅&amp;quot;, &amp;quot;阿佐ケ谷駅&amp;quot;, &amp;quot;荻窪駅&amp;quot;, &amp;quot;西荻窪駅&amp;quot;
)
idxs &amp;lt;- map_int(stations, station_to_idx)
AREA &amp;lt;- 25
AGE &amp;lt;- 0
WALK &amp;lt;- 5
FLOOR &amp;lt;- 3
IS_TOP &amp;lt;- 0
draws |&amp;gt;
filter(station_index %in% idxs) |&amp;gt;
# 駅名をプロットに表示するため
left_join(station_index_table, by=&amp;quot;station_index&amp;quot;) |&amp;gt;
mutate(station=factor(station, levels=rev(stations))) |&amp;gt;
mutate(
mu_exp=exp(
a + b*log(AREA) +
age*AGE +
walk*(WALK - 1) +
floor_num*max(FLOOR - 2, 0) +
is_top*IS_TOP +
is_ground*as.integer(FLOOR == 1L) +
is_underground*as.integer(FLOOR == -1L)
)
) |&amp;gt;
ggplot(aes(mu_exp, station))+
theme_minimal()+
tidybayes::stat_pointinterval(point_interval=tidybayes::median_qi, .width=0.95)+
theme(axis.title.y=element_blank(), axis.text=element_text(color=&amp;quot;black&amp;quot;))
&lt;/code&gt;&lt;/pre&gt;
&lt;/details&gt;
&lt;img src="images/chuo-1.png" style="width:80.0%" /&gt;
&lt;p&gt;黒い点は事後分布の中央値、点の左右にある棒は95%ベイズ信用区間です。25m2の新築、徒歩5分、3階の物件は最寄り駅が荻窪駅だと黒い点より13.1万円くらい、棒より95%の確率で13.0万円 -
13.2万円くらいだということを示します。&lt;/p&gt;
&lt;p&gt;なお、例えば築年数以外は同じ条件のまま築10年の家賃相場を考えてみると、築10年は新築と比べて約11%安くなることが分かっていますから、13.1万円
x 89% = 11万円後半になります。&lt;/p&gt;
&lt;p&gt;12万円台後半～13万円の高円寺から西荻窪は駅によって街の特徴が分かれるところですが、個人的には西荻窪は商店街が個性的なお店やおいしいお店が多く魅力的です。新宿まで15分ですし、10分で中野に出て東京メトロの東西線にも乗り換えられて交通の便もいいですね。隣が吉祥寺なので買い物に困ることもないですね。&lt;/p&gt;
&lt;p&gt;家賃相場の数値自体がどのくらい合っているかは評価が難しいところですが、最寄り駅ごとの相対的な違いとしては割と妥当に思われました。&lt;/p&gt;
&lt;p&gt;さて、いま示した「家賃相場」とは何でしょうか？今回設定したモデルの下では、「荻窪駅から徒歩5分、新築、25m2、3階の物件の家賃の平均的な値」という確率変数$\exp(\mu_{i})$があり、これを「家賃相場」と呼ぶと、家賃相場の中央値は13.1万円であり、家賃相場の確率分布の95%は13.0万円 -
13.2万円の間に入るということを示します。&lt;/p&gt;
&lt;p&gt;実際の物件の家賃は、この家賃相場にさらに$\sigma$というノイズが乗ったものとして観測される&lt;sup id="fnref:7"&gt;&lt;a href="#fn:7" class="footnote-ref" role="doc-noteref"&gt;7&lt;/a&gt;&lt;/sup&gt;ので、実際には13万円より安い物件も13.2万円より高い物件もありえます。ノイズには、バストイレ別かどうか、分譲賃貸かのようなモデルに入れていない特徴量や、その他説明が付かなかった物件固有のいろいろなものが含まれます。&lt;/p&gt;
&lt;h2 id="おわりに"&gt;おわりに&lt;/h2&gt;
&lt;p&gt;このモデルを参考にしながら物件を探してみたのですが、「掘り出し物の物件」というものはほとんどないんだなと思いました。グレードの高い物件はたいてい家賃相場から少し高めに設定されていましたし、家賃相場と比べて安い物件はエレベーターが付いていないなど、理由が何かしらありました。&lt;/p&gt;
&lt;p&gt;東京23区だけでも1ヶ月で100万件近いデータが得られたように賃貸マンション市場は非常に大きい市場であるため、競争が働いていて効率的な市場になっているということなんですね。&lt;/p&gt;
&lt;p&gt;なので掘り出し物の物件を見つけようというよりは、最寄り駅を変えるとどのくらい相場が変わるのかとか、築年数を10年下げる代わりに同じ家賃でどのくらい広い物件に住めるのかとか、物件探しのときの検討材料にするのがよさそうです。&lt;/p&gt;
&lt;p&gt;このような目的としては統計モデリングが非常に効果的ですね。予測精度としてはLightGBMのような機械学習の決定木の特徴を持つ手法が優れていますし、LightGBMでもPartial
Dependenceで似たような解釈ができます。&lt;/p&gt;
&lt;p&gt;しかし、統計モデルには、最寄り駅ごとに家賃相場が異なるというようなデータ生成のメカニズムを明示的にモデルに織り込むことでドメイン知識を活用できるメリットがあります。また、ベイズモデリングによって、複雑な統計モデルであってもある程度パラメータを推定しやすいことや、パラメータの信用区間という形で何パーセントの確率でパラメータはこの範囲内であるというパラメータの確信度合いを示せることが、解釈性の高さにつながっています。&lt;/p&gt;
&lt;p&gt;今後は築年数や駅徒歩分数の効果を非線形にするとか、家賃の外れ値のデータにロバストにするように正規分布ではなくt分布を導入するとか、モデルの高度化を進めてみたいです。&lt;/p&gt;
&lt;div class="footnotes" role="doc-endnotes"&gt;
&lt;hr&gt;
&lt;ol&gt;
&lt;li id="fn:1"&gt;
&lt;p&gt;例えば、全く同じ物件でも、マンション名が「○○マンション」のように明記されているページと、「○○駅徒歩x分築y年」のように明記されていないページで複数回登場することがあります。住所、最寄り駅と最寄駅からの分数、築年数、物件の階数、家賃が全く同じでマンション名だけ異なる物件が複数回ある場合は重複を除外するようにしました。&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:2"&gt;
&lt;p&gt;詳細は&lt;a href="../rent-modeling/#%e5%89%8d%e5%87%a6%e7%90%86"&gt;階層ベイズで東京23区のお部屋の家賃相場を推定する#前処理&lt;/a&gt;をご参照ください。&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:3"&gt;
&lt;p&gt;これらのデータは各物件の詳細ページに載っています。物件一覧ページは1ページに数十件の物件が載っているため高速にスクレイピングできますが、詳細ページは1ページ1件のため時間の制約上現実的にスクレイピングできないので断念しました。&amp;#160;&lt;a href="#fnref:3" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:4"&gt;
&lt;p&gt;1SLDKのように納戸のある物件もありますが、これも除外しています。納戸は居室には使えない部屋なので、納戸の面積は家賃に与える影響がその他の部屋と異なる可能性があるため、モデルからは除外しました。&amp;#160;&lt;a href="#fnref:4" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:5"&gt;
&lt;p&gt;一般的に、&lt;a href="https://ill-identified.hatenablog.com/entry/2019/06/13/010510#%E3%81%A9%E3%81%93%E3%82%92%E8%A6%8B%E3%81%A6%E5%8F%8E%E6%9D%9F%E3%82%92%E7%A2%BA%E8%AA%8D%E3%81%99%E3%82%8B%E3%81%8B" target="_blank" rel="noopener noreferrer"&gt;[R] [stan] bayesplot
を使ったモンテカルロ法の実践ガイド - ill-identified
diary&lt;/a&gt;のような内容をチェックします。詳細は&lt;a href="../rent-modeling/#%e6%8e%a8%e5%ae%9a%e7%b5%90%e6%9e%9c%e3%81%ae%e3%83%81%e3%82%a7%e3%83%83%e3%82%af"&gt;階層ベイズで東京23区のお部屋の家賃相場を推定する#推定結果のチェック&lt;/a&gt;をご参照ください。&amp;#160;&lt;a href="#fnref:5" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:6"&gt;
&lt;p&gt;他に最上階ではないという条件も設定していますが、これまでみたように最上階かどうかは家賃に影響を与えないので、最上階だとしてもプロットは変わりません。&amp;#160;&lt;a href="#fnref:6" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:7"&gt;
&lt;p&gt;正確には、$\log y_{i} \sim N(\mu_{i}, \sigma)$で生成される$y_{i}$のexpを取ったものです。$\exp(\mu_{i})$は信用区間、$y_{i}$は予測区間を求めているという違いです。&amp;#160;&lt;a href="#fnref:7" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</description></item><item><title>Rで画像をドット絵化する</title><link>https://suzunano.net/posts/pixel-art-by-r/</link><pubDate>Thu, 19 Dec 2024 00:00:00 +0900</pubDate><guid>https://suzunano.net/posts/pixel-art-by-r/</guid><description>&lt;h2 id="はじめに"&gt;はじめに&lt;/h2&gt;
&lt;p&gt;この記事は&lt;a href="https://qiita.com/advent-calendar/2024/rlang" target="_blank" rel="noopener noreferrer"&gt;R言語 Advent Calendar
2024&lt;/a&gt;の19日目の記事です。&lt;/p&gt;
&lt;p&gt;Rのパッケージ&lt;code&gt;imager&lt;/code&gt;を用いて、アニメ絵をドット絵に変換してみました。ドット絵作りたいな～と思い立ちまして、せっかくなので好きなRで実装してみました。&lt;/p&gt;
&lt;h2 id="ロジック"&gt;ロジック&lt;/h2&gt;
&lt;p&gt;ドット絵化のロジックはこちらです。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;画像を適当な幅でグリッドに切り、それぞれのグリッドについて、全ての画素のRGB値をグリッド内の画素のRGB値の平均値とする（平均プーリング）&lt;/li&gt;
&lt;li&gt;k-means法により、各グリッドのRGB値を、指定した色数でクラスタリングされた値に置き換える&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;1で色と色の境目をギザギザにして2で色数を減らすことでドット絵っぽさを出します。k-meansはこちらの記事（&lt;a href="https://zenn.dev/3w36zj6/articles/a1bd35a3c867a8" target="_blank" rel="noopener noreferrer"&gt;k-means法を用いて画像をドット絵風に変換する&lt;/a&gt;）にアイデアをもらいました。なお、2の前に1でも色数が減るため、2のk-meansの実行時間を抑えることができます。&lt;/p&gt;
&lt;p&gt;グリッドの縦と横のピクセル数とk-meansの色数はハイパーパラメータとして与えます。これらの値次第でドット絵の味わいが変わってきます。&lt;/p&gt;
&lt;p&gt;実装で使う&lt;code&gt;imager&lt;/code&gt;はC++のCImgをラップしたパッケージです。画像処理分野では他にもImageMagickのラッパーの&lt;code&gt;magick&lt;/code&gt;やOpenCVのラッパーの&lt;code&gt;Rvision&lt;/code&gt;などいくつかパッケージがあるようです。今回の内容は配列操作で完結するので何を使ってもいいと思います。&lt;/p&gt;
&lt;h2 id="実装"&gt;実装&lt;/h2&gt;
&lt;p&gt;アニメ「&lt;a href="https://www.amazon.co.jp/dp/B0B8RZRP27" target="_blank" rel="noopener noreferrer"&gt;スローループ&lt;/a&gt;」の海凪小春ちゃんです&lt;sup id="fnref:1"&gt;&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref"&gt;1&lt;/a&gt;&lt;/sup&gt;。この画像をドット絵にします。（©うちのまいこ・芳文社／スローループ製作委員会）&lt;/p&gt;
&lt;img src="./slowloop_12.jpg" width="800px"&gt;
&lt;h3 id="画像の読み込み"&gt;画像の読み込み&lt;/h3&gt;
&lt;p&gt;環境はR4.4.2, imager1.0.2です。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-r"&gt;# install.packages(c(&amp;quot;tidyverse&amp;quot;, &amp;quot;imager&amp;quot;))
library(tidyverse)
library(imager)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;まず、&lt;code&gt;imager::load.image()&lt;/code&gt;で画像を読み込みます。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-r"&gt;img &amp;lt;- imager::load.image(&amp;quot;slowloop.jpg&amp;quot;)
plot(img)
&lt;/code&gt;&lt;/pre&gt;
&lt;img src="images/unnamed-chunk-3-1.png" style="width:100.0%" /&gt;
&lt;p&gt;&lt;code&gt;imager::load.image()&lt;/code&gt;で読み込まれた画像は&lt;code&gt;cimg&lt;/code&gt;というS3クラスであり、その実体はwidth
x height x depth（静止画なら1、動画ならフレーム数） x color
channel（透過度（アルファチャンネル）がないカラー画像なら3）の4次元arrayです。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-r"&gt;img
#&amp;gt; Image. Width: 1920 pix Height: 1080 pix Depth: 1 Colour channels: 3
str(img)
#&amp;gt; 'cimg' num [1:1920, 1:1080, 1, 1:3] 0.863 0.863 0.863 0.863 0.863 ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;1920px x 1080pxの画像であることが分かります。&lt;/p&gt;
&lt;p&gt;実体はarrayなので&lt;code&gt;dim&lt;/code&gt;のような配列操作の関数が使えますし、&lt;code&gt;as.array()&lt;/code&gt;するとarrayを得ることができます。&lt;/p&gt;
&lt;p&gt;試しに左上の座標が(101, 101)、右下の座標が(105,
105)の長方形の領域を取り出してみます。R, G,
Bの値が4次元で入っていることが分かります。（255で割って0-1にスケーリングされた値が入っています）&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-r"&gt;as.array(img)[101:105, 101:105, 1, , drop=FALSE]
#&amp;gt; , , 1, 1
#&amp;gt;
#&amp;gt; [,1] [,2] [,3] [,4] [,5]
#&amp;gt; [1,] 0.7333333 0.7215686 0.7137255 0.7058824 0.6980392
#&amp;gt; [2,] 0.7333333 0.7176471 0.7098039 0.7019608 0.6980392
#&amp;gt; [3,] 0.7333333 0.7176471 0.7098039 0.7019608 0.6941176
#&amp;gt; [4,] 0.7294118 0.7137255 0.7098039 0.7019608 0.6941176
#&amp;gt; [5,] 0.7254902 0.7137255 0.7098039 0.7058824 0.6941176
#&amp;gt;
#&amp;gt; , , 1, 2
#&amp;gt;
#&amp;gt; [,1] [,2] [,3] [,4] [,5]
#&amp;gt; [1,] 0.8352941 0.8313725 0.8235294 0.8196078 0.8117647
#&amp;gt; [2,] 0.8352941 0.8274510 0.8196078 0.8156863 0.8117647
#&amp;gt; [3,] 0.8352941 0.8274510 0.8196078 0.8156863 0.8078431
#&amp;gt; [4,] 0.8313725 0.8235294 0.8196078 0.8156863 0.8078431
#&amp;gt; [5,] 0.8235294 0.8235294 0.8196078 0.8196078 0.8078431
#&amp;gt;
#&amp;gt; , , 1, 3
#&amp;gt;
#&amp;gt; [,1] [,2] [,3] [,4] [,5]
#&amp;gt; [1,] 0.9803922 0.9843137 0.9764706 0.9764706 0.9764706
#&amp;gt; [2,] 0.9803922 0.9803922 0.9764706 0.9725490 0.9764706
#&amp;gt; [3,] 0.9803922 0.9803922 0.9764706 0.9803922 0.9725490
#&amp;gt; [4,] 0.9764706 0.9764706 0.9764706 0.9803922 0.9803922
#&amp;gt; [5,] 0.9803922 0.9764706 0.9764706 0.9843137 0.9803922
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id="平均プーリングによる減色"&gt;平均プーリングによる減色&lt;/h3&gt;
&lt;p&gt;1920px x
1080pxの元の画像を、縦横15pxずつのグリッドに切って、各グリッドのRGB値を平均値に置き換えます。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-r"&gt;# グリッドの縦と横の幅
row_px &amp;lt;- 15
col_px &amp;lt;- 15
width &amp;lt;- dim(img)[1]
height &amp;lt;- dim(img)[2]
# 空のarrayを用意しておく
# 1は静止画なので、3はカラー画像なので
img_average &amp;lt;- array(NA_real_, dim=c(width, height, 1, 3))
for (x0 in seq(1, width, by=col_px)) {
for (y0 in seq(1, height, by=row_px)) {
# x1 &amp;lt;- x0-1+col_pxだと元の画像がグリッドで割り切れないときにエラーになる
x1 &amp;lt;- min(x0-1+col_px, width)
y1 &amp;lt;- min(y0-1+row_px, height)
img_block &amp;lt;- img[x0:x1, y0:y1, 1, 1:3, drop=FALSE]
img_average[x0:x1, y0:y1, 1, 1] &amp;lt;- mean(img_block[, , 1, 1])
img_average[x0:x1, y0:y1, 1, 2] &amp;lt;- mean(img_block[, , 1, 2])
img_average[x0:x1, y0:y1, 1, 3] &amp;lt;- mean(img_block[, , 1, 3])
}
}
img_average &amp;lt;- imager::as.cimg(img_average)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;img_average &amp;lt;- as.cimg(img_average)&lt;/code&gt;の部分ですが、width x height x
depth x color
channelの4次元arrayを&lt;code&gt;imager::as.cimg()&lt;/code&gt;すると&lt;code&gt;cimg&lt;/code&gt;オブジェクトに変換できます。&lt;/p&gt;
&lt;h3 id="k-means"&gt;k-means&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;cimg&lt;/code&gt;オブジェクトを&lt;code&gt;as.data.frame()&lt;/code&gt;すると、&lt;code&gt;x&lt;/code&gt;,
&lt;code&gt;y&lt;/code&gt;におけるカラーチャンネルの画素値がdata.frameで得られます。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-r"&gt;as.data.frame(img_average) |&amp;gt;
head(5)
#&amp;gt; x y cc value
#&amp;gt; 1 1 1 1 0.8627451
#&amp;gt; 2 2 1 1 0.8627451
#&amp;gt; 3 3 1 1 0.8627451
#&amp;gt; 4 4 1 1 0.8627451
#&amp;gt; 5 5 1 1 0.8627451
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;このdata.frameの状態で&lt;code&gt;stats::kmeans&lt;/code&gt;を実行します。ただしさきほど平均プーリングをかけたので、高速化のために画素値が異なっている行のみを残してからk-meansを実行します。&lt;/p&gt;
&lt;p&gt;使える色は16色にしてみます。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-r"&gt;# k-meansのパラメータ
k &amp;lt;- 16
nstart &amp;lt;- 1234
# 扱いやすくするためにdata.frameに変換する
img_average_df &amp;lt;- as.data.frame(img_average) |&amp;gt;
# pivot_widerで横持ちにすると`1`, `2`, `3`という列ができるので
# 先頭にccを付けておくことでselectなどするときにバッククオートで囲わなくてすむ
mutate(cc=str_c(&amp;quot;cc&amp;quot;, cc)) |&amp;gt;
pivot_wider(names_from=cc, values_from=value)
# ユニークなピクセルだけでk-meansを実行することで高速化する
# ユニークなピクセルを識別するidを付けておく
img_unique_df &amp;lt;- img_average_df |&amp;gt;
distinct(cc1, cc2, cc3, .keep_all=TRUE) |&amp;gt;
select(cc1, cc2, cc3) |&amp;gt;
mutate(id=row_number())
img_average_df &amp;lt;- img_average_df |&amp;gt;
left_join(img_unique_df, by=c(&amp;quot;cc1&amp;quot;, &amp;quot;cc2&amp;quot;, &amp;quot;cc3&amp;quot;))
# k-meansを実行する
res_kmeans &amp;lt;- kmeans(
img_unique_df |&amp;gt;
select(&amp;quot;cc1&amp;quot;, &amp;quot;cc2&amp;quot;, &amp;quot;cc3&amp;quot;),
centers=k, nstart=nstart, iter.max=1000, algorithm=&amp;quot;MacQueen&amp;quot;
)
img_kmeans_df &amp;lt;- data.frame(
res_kmeans$centers[res_kmeans$cluster, ]
) |&amp;gt;
as_tibble() |&amp;gt;
set_names(c(&amp;quot;cc1_k&amp;quot;, &amp;quot;cc2_k&amp;quot;, &amp;quot;cc3_k&amp;quot;)) |&amp;gt;
mutate(id=row_number())
# 元の平均プーリングした画像のそれぞれのピクセルのRGB値にk-meansしたあとのRGB値をつける
res &amp;lt;- left_join(img_average_df, img_kmeans_df, by=&amp;quot;id&amp;quot;) |&amp;gt;
select(x, y, cc1_k, cc2_k, cc3_k) |&amp;gt;
pivot_longer(c(cc1_k, cc2_k, cc3_k), names_to=&amp;quot;cc&amp;quot;) |&amp;gt;
mutate(cc=case_when(
cc == &amp;quot;cc1_k&amp;quot; ~ 1L,
cc == &amp;quot;cc2_k&amp;quot; ~ 2L,
cc == &amp;quot;cc3_k&amp;quot; ~ 3L
))
# x, y, cc, valueを列に持つdata.frameを`imager::as.cimg()`すると`cimg`オブジェクトに変換できる
res &amp;lt;- imager::as.cimg(res, dim=c(width, height, 1, 3))
imager::save.image(res, &amp;quot;output.jpg&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id="結果"&gt;結果&lt;/h2&gt;
&lt;p&gt;結果です。だいたい20秒くらいで作れました。&lt;/p&gt;
&lt;img src="./slowloop_12_15x15_16.png" width="800px"&gt;
&lt;p&gt;ドット絵感が出てますね！&lt;/p&gt;
&lt;p&gt;今の画像は16色ですが、8色に絞ってみます。&lt;/p&gt;
&lt;img src="./slowloop_12_15x15_8.png" width="800px"&gt;
&lt;p&gt;ドット絵感はより強くなりましたが、使える色数が減ったのでほっぺたや手の指の境目など一部色が消えていますね。&lt;/p&gt;
&lt;p&gt;もっとグリッドを小さくしたり色数を増やしたりすると絵はきれいになりますがドット絵感が薄れます。グリッドの幅と高さや色数の最適な値は元の画像によって異なります。書き込みが細かい画像であればグリッドを小さくしたり色数を増やしたりする方が上手に作れます。&lt;/p&gt;
&lt;h2 id="おわりに"&gt;おわりに&lt;/h2&gt;
&lt;p&gt;シンプルなアルゴリズムですが、いい感じのドット絵を作ることができました。Rで画像処理も楽しいですね。&lt;/p&gt;
&lt;h2 id="関連リンク"&gt;関連リンク&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://zenn.dev/3w36zj6/articles/a1bd35a3c867a8" target="_blank" rel="noopener noreferrer"&gt;k-means法を用いて画像をドット絵風に変換する&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;ドット絵化のロジックを参考にさせていただきました。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="../line-drawing/"&gt;アニメのキャプチャ画像から線画を作る&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;小春ちゃんには以前書いたこちらの記事でも登場してもらいました。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="footnotes" role="doc-endnotes"&gt;
&lt;hr&gt;
&lt;ol&gt;
&lt;li id="fn:1"&gt;
&lt;p&gt;&lt;a href="https://slowlooptv.com/" target="_blank" rel="noopener noreferrer"&gt;アニメの公式サイト&lt;/a&gt;を貼りたかったのですが、KADOKAWAが不正アクセスを受けた日以来サイトが閉まっていていまだにアクセスできなくて悲しい&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</description></item><item><title>Realized Volatilityの理論と実装</title><link>https://suzunano.net/posts/realized-volatility/</link><pubDate>Tue, 10 Dec 2024 00:00:00 +0900</pubDate><guid>https://suzunano.net/posts/realized-volatility/</guid><description>&lt;h2 id="はじめに"&gt;はじめに&lt;/h2&gt;
&lt;p&gt;この記事は&lt;a href="https://qiita.com/advent-calendar/2024/market-api" target="_blank" rel="noopener noreferrer"&gt;マケデコ Advent Calendar
2024&lt;/a&gt;の10日目の記事です。&lt;/p&gt;
&lt;p&gt;Realized Volatility (RV)
という、金融商品の価格のボラティリティの推定量があります。&lt;/p&gt;
&lt;p&gt;RVはティックレベル～分足レベルの高頻度の価格データから計算できます。モデルフリーで精度のよい日次ボラティリティの推定量であることが知られているとともに、連続な価格変動によるボラティリティとジャンプによるボラティリティを分離するように拡張できるのがメリットです。&lt;/p&gt;
&lt;p&gt;この記事ではまずRVの理論をざっくり解説したあと、GMOコインのAPIからドル円の分足データを取得してドル円のRVの計算を実装します。&lt;/p&gt;
&lt;p&gt;為替介入があった日にジャンプによるボラティリティが大きかったことが確認できました。これは、RVによるボラティリティの推定と、ジャンプ拡散過程を考慮することによるボラティリティの連続成分とジャンプ成分の分離が有用なことを示唆します。&lt;/p&gt;
&lt;h2 id="理論"&gt;理論&lt;/h2&gt;
&lt;p&gt;ボラティリティはリスク管理上などで重要な変数ですが真の値は直接観測できない潜在変数であるため、その推定方法は数理ファイナンスの主要な分野となっています。&lt;/p&gt;
&lt;p&gt;一定期間の日次収益率のローリング標準偏差とする方法はシンプルながらよく用いられる方法ですが、ローリングウィンドウの間はボラティリティが一定であることを仮定するため、ボラティリティの日々のシャープな変動をとらえることができません。&lt;/p&gt;
&lt;p&gt;日々変動するボラティリティを推定するため、日次のボラティリティを数理モデルで表し、日次の収益率からボラティリティを推定する方法があります。有名なGARCHモデルや、状態空間モデルの一種であるStochastic
Volatility (SV)
モデルはこれに該当します。なお、&lt;a href="../stochastic-volatility-model"&gt;昨年のアドベントカレンダーの記事&lt;/a&gt;ではSVモデルをStanで実装してみました。興味があれば読んでみてください。&lt;/p&gt;
&lt;p&gt;一方、高頻度の価格データを用いることでボラティリティの推定量を直接求めるアプローチがあります。特に2000年代～2010年代に注目された分野です。最も代表的な推定量であるAndersen
and Bolleslev (1998) が示したRVについて、簡単に解説します。&lt;/p&gt;
&lt;h3 id="連続過程"&gt;連続過程&lt;/h3&gt;
&lt;p&gt;ある時点$s$における資産の対数価格を$p(s)$とします。&lt;/p&gt;
&lt;p&gt;いま、$t$日に等間隔に$n$回価格が観測されるとして&lt;sup id="fnref:1"&gt;&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref"&gt;1&lt;/a&gt;&lt;/sup&gt;、$t$日の観測時点$i$における日中収益率を&lt;/p&gt;
&lt;p&gt;$$
r_{t, i} = p(t-1 + i/n) - p(t-1 + (i-1)/n), \quad i=1, 2, \dots, n
$$&lt;/p&gt;
&lt;p&gt;と定義します。要するに1時点前との対数収益率です。&lt;/p&gt;
&lt;p&gt;このとき、$t$日のRVは以下で求められます。&lt;/p&gt;
&lt;p&gt;$$
RV_{t} = \sum_{i=1}^n r_{t, i}^2
$$&lt;/p&gt;
&lt;p&gt;日中収益率をそれぞれ2乗して足し合わせただけですね。&lt;/p&gt;
&lt;p&gt;RVは真のボラティリティの一致推定量であることが知られています&lt;sup id="fnref:2"&gt;&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref"&gt;2&lt;/a&gt;&lt;/sup&gt;。簡単な計算方法に見えますが、対数価格がセミマルチンゲールな確率過程に従うとき、RVは真のボラティリティの一致推定量であるという理論的な裏付けがあります。&lt;/p&gt;
&lt;p&gt;いま具体的に、$p(s)$が以下の確率過程に従うとします&lt;sup id="fnref:3"&gt;&lt;a href="#fn:3" class="footnote-ref" role="doc-noteref"&gt;3&lt;/a&gt;&lt;/sup&gt;。$W(s)$は標準ブラウン運動です。&lt;/p&gt;
&lt;p&gt;$$
dp(s) = \mu(s) ds + \sigma(s) dW(s)
$$&lt;/p&gt;
&lt;p&gt;$t$日における真のボラティリティ$IV_{t}$ (Integrated Volatility; IV)
は、瞬間的なボラティリティ$\sigma(s)$を一日分積分したものです。&lt;/p&gt;
&lt;p&gt;$$
IV_{t} = \int_{t-1}^{t} \sigma(s)^2 ds
$$&lt;/p&gt;
&lt;p&gt;ただし、積分範囲の$t-1, t$は、それぞれ$t-1$日,
$t$日の最後の観測時点であることを示します。&lt;/p&gt;
&lt;p&gt;ここで問題になるのは、$\sigma(s)$は直接観測できないパラメータなので$IV_{t}$も直接観測できないということです。そのため、何かしらの方法で推定値を求めることになります。&lt;/p&gt;
&lt;p&gt;$n \rightarrow \infty$のとき$RV_{t} \rightarrow IV_{t}$となるため&lt;sup id="fnref:4"&gt;&lt;a href="#fn:4" class="footnote-ref" role="doc-noteref"&gt;4&lt;/a&gt;&lt;/sup&gt;、$n$を大きく取るとき、RVはIVの精度のよい一致推定量となります。&lt;/p&gt;
&lt;p&gt;なお、$t$日における対前日の対数収益率を$r_{t}$とすると、以下の$\sigma_{t}$が$t$日における真のボラティリティです。&lt;/p&gt;
&lt;p&gt;$$
\begin{aligned}
r_{t} &amp;amp;= E_{t-1} [r_{t}] + \epsilon_{t} \\
\epsilon_{t} &amp;amp;= \sigma_{t} z_{t}, \quad \sigma_{t} &amp;gt; 0, \quad z_{t} \sim i.i.d., \quad E[z_{t}] = 0, \quad Var[z_{t}] = 1
\end{aligned}
$$&lt;/p&gt;
&lt;p&gt;$E_{t-1} [r_{t}]= 0, z_{t} \sim N(0, 1)$とすれば$r_{t} \sim N(0, \sigma_{t}^2)$です。$RV_{t}$は$\sigma_{t}^2$であるというとRVとは何かイメージしやすいのではないでしょうか。&lt;/p&gt;
&lt;h3 id="ジャンプ過程"&gt;ジャンプ過程&lt;/h3&gt;
&lt;p&gt;実際の対数価格は連続な確率過程ではなく、しばしば不連続なジャンプを含みます。以上の議論をジャンプ過程に拡張してみましょう。&lt;/p&gt;
&lt;p&gt;$p(s)$は以下のジャンプ拡散過程に従うとします。&lt;/p&gt;
&lt;p&gt;$$
dp(s) = \mu(s) ds + \sigma(s) dW(s) + \kappa(s) dN(s)
$$&lt;/p&gt;
&lt;p&gt;$N(s)$は時点$s$でジャンプがある場合$dN(s) = 1$、ない場合$dN(s) = 0$となるポアソン過程です。$\kappa(s)$は時点$s$におけるジャンプの大きさを表します。&lt;/p&gt;
&lt;p&gt;連続の場合と同様に、$RV_{t} = \sum_{i=1}^n r_{t, i}^2$とすると、$n \rightarrow \infty$のとき、&lt;/p&gt;
&lt;p&gt;$$
RV_{t} \rightarrow \int_{t - 1}^{t} \sigma(s)^2 ds + \sum_{t-1 &amp;lt; s \leq t} \kappa(s)^2
$$&lt;/p&gt;
&lt;p&gt;となります。&lt;/p&gt;
&lt;p&gt;右辺第1項と右辺第2項はそれぞれ連続 (Continuous)
な価格変動によるボラティリティとジャンプ (Jump)
の価格変動によるボラティリティです。それぞれの推定量を$C_{t}, J_{t}$と表します。&lt;/p&gt;
&lt;h3 id="ジャンプ過程-ジャンプ部分に由来するボラティリティの分離"&gt;ジャンプ過程: ジャンプ部分に由来するボラティリティの分離&lt;/h3&gt;
&lt;p&gt;$C_{t}, J_{t}$は異なる性質を持つリスクなので、$RV_{t}$を$C_{t}, J_{t}$に分解できると嬉しいことがありそうです。分解の方法については提案されている手法がいくつかありますが、Barndorff-Nielsen
and Shephard (2004, 2006) の方法を説明します。&lt;/p&gt;
&lt;p&gt;次の式の$BV_{t}$ (Bipower Variation; BV)
は$n \rightarrow \infty$で連続部分のボラティリティに確率収束します。&lt;/p&gt;
&lt;p&gt;$$
BV_{t} = \mu_{1}^{-2} \sum_{i=2}^{n} |r_{t, i}| |r_{t, i-1}|
$$&lt;/p&gt;
&lt;p&gt;ただし、$\mu_{1} = 2^{1/2} \Gamma(1) \Gamma(1/2)^{-1}$です。&lt;/p&gt;
&lt;p&gt;以上より、$n \rightarrow \infty$のとき、$C_{t} = BV_{t}, J_{t} = \max(RV_{t} - BV_{t}, 0)$となります。&lt;/p&gt;
&lt;p&gt;ただし、ジャンプの存在が有意かどうかを検定することが実証上は多いです。ジャンプが存在しないという帰無仮説のもとでは、以下の統計量$Z_{t}$は漸近的に標準正規分布に従います。&lt;/p&gt;
&lt;p&gt;$$
Z_{t} = \frac{\log RV_{t} - \log BV_{t}}{((\mu_{1}^{-4} + 2\mu_{1}^{-2} - 5) TQ_{t} BV_{t}^{-2} / n)^{1/2}}
$$&lt;/p&gt;
&lt;p&gt;ただし、$TQ_{t}$ (Tri-power Quarticity; TQ) は&lt;/p&gt;
&lt;p&gt;$$
TQ_{t} = n \mu_{4/3}^{-3} \sum_{i=3}^{n} |r_{t, i}|^{4/3} |r_{t, i-1}|^{4/3} |r_{t, i-2}|^{4/3}
$$&lt;/p&gt;
&lt;p&gt;であり、$\mu_{4/3} = 2^{2/3} \Gamma(7/6) \Gamma(1/2)^{-1}$です。&lt;/p&gt;
&lt;p&gt;有意水準を5%のように設定して&lt;sup id="fnref:5"&gt;&lt;a href="#fn:5" class="footnote-ref" role="doc-noteref"&gt;5&lt;/a&gt;&lt;/sup&gt;、帰無仮説が棄却されれば$J_{t} = RV_{t} - BV_{t}$、棄却されなければ$J_{t} = 0$とします。&lt;/p&gt;
&lt;h3 id="nの設定"&gt;nの設定&lt;/h3&gt;
&lt;p&gt;以上の議論は$n \rightarrow \infty$で行ってきたので、実装する際にはティックデータを用いるのが一見最もよさそうに思われます。&lt;/p&gt;
&lt;p&gt;しかし、実際のマーケットでは、価格には様々なノイズ（マイクロマーケットストラクチャーノイズといいます）が含まれています。一例を挙げると、価格には呼値が設定されていますが、最も高い買値で約定するか最も安い売値で約定するかによって、真の価格は変わらなくても実際の価格はBidとAskを行ったり来たりします。なのでnを細かくするほどRVにはノイズが多く含まれ、IVの一致推定量としての精度が落ちます。&lt;/p&gt;
&lt;p&gt;ではノイズを考慮してnをどの程度に取るのが望ましいかですが、アセットクラスや流動性によって多少異なるものの、RVでは5分間隔の価格データを用いるのがおおむねよいとされます
(Liu, Patton and Sheppard
(2015))。以前より実証では5分がよく用いられていたのですが、5分のRVは他の時間間隔や他のボラティリティの推定量と比べてだいぶいい推定精度のパフォーマンスを示すことがこの論文で示されました&lt;sup id="fnref:6"&gt;&lt;a href="#fn:6" class="footnote-ref" role="doc-noteref"&gt;6&lt;/a&gt;&lt;/sup&gt;。&lt;/p&gt;
&lt;h2 id="実装"&gt;実装&lt;/h2&gt;
&lt;p&gt;RVを実装してみましょう。上の結果の導出は難しいですが、結果の実装は難しくありません。&lt;/p&gt;
&lt;h3 id="データの取得"&gt;データの取得&lt;/h3&gt;
&lt;p&gt;&lt;a href="https://api.coin.z.com/fxdocs/#outline" target="_blank" rel="noopener noreferrer"&gt;GMOコインが為替の分足データをAPI経由で無料で提供しています&lt;/a&gt;。ここからドル円の5分足をJSTの2023/10/30から2024/12/6まで取得します。分足が簡単に手に入りレスポンスも速い、いいAPIです。&lt;/p&gt;
&lt;p&gt;環境はpython=3.11.5, polars=1.16.0, plotnine=0.14.3です。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-python"&gt;import datetime
import json
import math
import time
import plotnine as pn
import polars as pl
import requests
import scipy as sp
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class="language-python"&gt;endpoint = &amp;quot;https://forex-api.coin.z.com/public/v1/klines&amp;quot;
dates = [i.strftime(&amp;quot;%Y%m%d&amp;quot;) for i in pl.date_range(
start=datetime.date(2023, 10, 28),
end=datetime.date(2024, 12, 6),
interval=&amp;quot;1d&amp;quot;,
eager=True
)]
res = []
for date in dates:
params = {
&amp;quot;symbol&amp;quot;: &amp;quot;USD_JPY&amp;quot;,
&amp;quot;priceType&amp;quot;: &amp;quot;ASK&amp;quot;,
&amp;quot;interval&amp;quot;: &amp;quot;5min&amp;quot;,
&amp;quot;date&amp;quot;: date,
}
resp = requests.get(endpoint, params=params)
# データが存在しない日（市場が開いていない日）は空のリスト(&amp;quot;[]&amp;quot;)が返る
res.append(pl.DataFrame(json.loads(resp.text)[&amp;quot;data&amp;quot;]))
time.sleep(1)
df = pl.concat([i for i in res if not i.is_empty()])
df = (
df
.with_columns(
open=pl.col(&amp;quot;open&amp;quot;).cast(float),
high=pl.col(&amp;quot;high&amp;quot;).cast(float),
low=pl.col(&amp;quot;low&amp;quot;).cast(float),
close=pl.col(&amp;quot;close&amp;quot;).cast(float),
)
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;取得したデータを&lt;code&gt;polars.DataFrame&lt;/code&gt;で持ちます。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;shape: (82_248, 5)
┌───────────────┬─────────┬─────────┬─────────┬─────────┐
│ openTime ┆ open ┆ high ┆ low ┆ close │
│ --- ┆ --- ┆ --- ┆ --- ┆ --- │
│ str ┆ f64 ┆ f64 ┆ f64 ┆ f64 │
╞═══════════════╪═════════╪═════════╪═════════╪═════════╡
│ 1698616800000 ┆ 149.641 ┆ 149.685 ┆ 149.641 ┆ 149.669 │
│ 1698617100000 ┆ 149.669 ┆ 149.669 ┆ 149.636 ┆ 149.644 │
│ 1698617400000 ┆ 149.644 ┆ 149.677 ┆ 149.638 ┆ 149.67 │
│ … ┆ … ┆ … ┆ … ┆ … │
│ 1733517900000 ┆ 150.103 ┆ 150.123 ┆ 150.096 ┆ 150.105 │
│ 1733518200000 ┆ 150.105 ┆ 150.109 ┆ 150.075 ┆ 150.105 │
│ 1733518500000 ┆ 150.106 ┆ 150.111 ┆ 150.091 ┆ 150.096 │
└───────────────┴─────────┴─────────┴─────────┴─────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;openTime&lt;/code&gt;列はUTCのエポックミリ秒なので、JSTに直します。&lt;/p&gt;
&lt;p&gt;次に、RVなどの計算において何時から何時までを1日として扱うかですが、APIのパスパラメータで&lt;code&gt;date=yyyymmdd&lt;/code&gt;を指定すると、JSTの&lt;code&gt;yyyymmdd&lt;/code&gt;の6:00-翌日5:59（月曜日はJST7:00-5:59）のデータが得られるので、JSTの6:00-5:59を1日とすることにします。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-python"&gt;df = (
df
.rename({&amp;quot;openTime&amp;quot;: &amp;quot;openTimeUtc&amp;quot;})
.with_columns(
timestamp_utc=pl.from_epoch(pl.col(&amp;quot;openTimeUtc&amp;quot;).cast(int), time_unit=&amp;quot;ms&amp;quot;)
)
.with_columns(
timestamp_jst=pl.col(&amp;quot;timestamp_utc&amp;quot;)+datetime.timedelta(hours=9),
timestamp=pl.col(&amp;quot;timestamp_utc&amp;quot;)+datetime.timedelta(hours=9)-datetime.timedelta(hours=6)
)
.with_columns(date=pl.col(&amp;quot;timestamp&amp;quot;).dt.date())
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;こんなデータができました。表示上の都合で一部の列に絞っています。&lt;/p&gt;
&lt;p&gt;JSTの2023/10/30の7:00-7:05は始値が149.641,
終値が149.669ということを示します。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;shape: (82_248, 6)
┌─────────┬─────────┬─────────┬─────────┬─────────────────────┬────────────┐
│ open ┆ high ┆ low ┆ close ┆ timestamp_jst ┆ date │
│ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- │
│ f64 ┆ f64 ┆ f64 ┆ f64 ┆ datetime[ms] ┆ date │
╞═════════╪═════════╪═════════╪═════════╪═════════════════════╪════════════╡
│ 149.641 ┆ 149.685 ┆ 149.641 ┆ 149.669 ┆ 2023-10-30 07:00:00 ┆ 2023-10-30 │
│ 149.669 ┆ 149.669 ┆ 149.636 ┆ 149.644 ┆ 2023-10-30 07:05:00 ┆ 2023-10-30 │
│ 149.644 ┆ 149.677 ┆ 149.638 ┆ 149.67 ┆ 2023-10-30 07:10:00 ┆ 2023-10-30 │
│ … ┆ … ┆ … ┆ … ┆ … ┆ … │
│ 150.103 ┆ 150.123 ┆ 150.096 ┆ 150.105 ┆ 2024-12-07 05:45:00 ┆ 2024-12-06 │
│ 150.105 ┆ 150.109 ┆ 150.075 ┆ 150.105 ┆ 2024-12-07 05:50:00 ┆ 2024-12-06 │
│ 150.106 ┆ 150.111 ┆ 150.091 ┆ 150.096 ┆ 2024-12-07 05:55:00 ┆ 2024-12-06 │
└─────────┴─────────┴─────────┴─────────┴─────────────────────┴────────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id="rvの実装"&gt;RVの実装&lt;/h3&gt;
&lt;p&gt;前のセクションで述べた結果を実装します。ジャンプの検定に使う有意水準は5%としています。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-python"&gt;mu_1 = 2**(1/2) * math.gamma(1) * math.gamma(1/2)**(-1)
mu_4over3 = 2**(2/3) * math.gamma(7/6) * math.gamma(1/2)**(-1)
alpha = 0.95
df_volatility = (
df
# 収益率は100倍して%表記にする
.with_columns(ret=(pl.col(&amp;quot;close&amp;quot;).log() - pl.col(&amp;quot;close&amp;quot;).shift(1).log()) * 100)
.group_by(&amp;quot;date&amp;quot;)
.agg(
n=pl.len(),
rv=(pl.col(&amp;quot;ret&amp;quot;)**2).sum(),
bv=mu_1**(-2) * (pl.col(&amp;quot;ret&amp;quot;).abs() * pl.col(&amp;quot;ret&amp;quot;).shift(1).abs()).sum(),
tq=pl.len() * mu_4over3**(-3) * (pl.col(&amp;quot;ret&amp;quot;).abs()**(4/3) * pl.col(&amp;quot;ret&amp;quot;).shift(1).abs()**(4/3) * pl.col(&amp;quot;ret&amp;quot;).shift(2).abs()**(4/3)).sum(),
)
.sort(&amp;quot;date&amp;quot;)
.with_columns(
z=(pl.col(&amp;quot;rv&amp;quot;).log() - pl.col(&amp;quot;bv&amp;quot;).log()) / ((mu_1**(-4) + 2 * mu_1**(-2) - 5) * pl.col(&amp;quot;tq&amp;quot;) * pl.col(&amp;quot;bv&amp;quot;)**(-2) / pl.col(&amp;quot;n&amp;quot;))**(1/2)
)
.with_columns(
j=pl.when(pl.col(&amp;quot;z&amp;quot;) &amp;gt; sp.stats.norm.ppf(alpha)).then(pl.col(&amp;quot;rv&amp;quot;) - pl.col(&amp;quot;bv&amp;quot;)).otherwise(pl.lit(0))
)
.with_columns(
c=pl.col(&amp;quot;rv&amp;quot;) - pl.col(&amp;quot;j&amp;quot;)
)
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;以下のとおり求められました。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;shape: (288, 8)
┌────────────┬─────┬──────────┬──────────┬──────────┬───────────┬──────────┬──────────┐
│ date ┆ n ┆ rv ┆ bv ┆ tq ┆ z ┆ j ┆ c │
│ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- │
│ date ┆ u32 ┆ f64 ┆ f64 ┆ f64 ┆ f64 ┆ f64 ┆ f64 │
╞════════════╪═════╪══════════╪══════════╪══════════╪═══════════╪══════════╪══════════╡
│ 2023-10-30 ┆ 276 ┆ 0.199465 ┆ 0.094773 ┆ 0.012525 ┆ 13.415503 ┆ 0.104692 ┆ 0.094773 │
│ 2023-10-31 ┆ 288 ┆ 0.374825 ┆ 0.30842 ┆ 0.333002 ┆ 2.266396 ┆ 0.066405 ┆ 0.30842 │
│ 2023-11-01 ┆ 288 ┆ 0.194893 ┆ 0.19418 ┆ 0.086115 ┆ 0.052693 ┆ 0.0 ┆ 0.194893 │
│ … ┆ … ┆ … ┆ … ┆ … ┆ … ┆ … ┆ … │
│ 2024-12-04 ┆ 288 ┆ 0.487489 ┆ 0.446522 ┆ 0.22751 ┆ 1.786992 ┆ 0.040967 ┆ 0.446522 │
│ 2024-12-05 ┆ 288 ┆ 0.471903 ┆ 0.458031 ┆ 0.306132 ┆ 0.537139 ┆ 0.0 ┆ 0.471903 │
│ 2024-12-06 ┆ 288 ┆ 0.486899 ┆ 0.455146 ┆ 0.425198 ┆ 1.023669 ┆ 0.0 ┆ 0.486899 │
└────────────┴─────┴──────────┴──────────┴──────────┴───────────┴──────────┴──────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;polarsはめちゃくちゃ読みやすいですね。polarsは大きなDataFrameを高速に処理できることがメリットとしてよく言われますが、認知負荷にやさしい構文なのが一番好きなポイントです。Rのtidyverseに近い書き方なのでtidyverseで育ったわたしにとっても使いやすいです。&lt;/p&gt;
&lt;h3 id="プロット"&gt;プロット&lt;/h3&gt;
&lt;p&gt;RVは%表記です。積み上げ面グラフで連続部分$C_{t}$とジャンプ部分$J_{t}$に分けています。&lt;/p&gt;
&lt;details class="code-fold"&gt;
&lt;summary&gt;Code&lt;/summary&gt;
&lt;pre&gt;&lt;code class="language-python"&gt;p1 = (
pn.ggplot(
df_volatility
.select(&amp;quot;date&amp;quot;, &amp;quot;j&amp;quot;, &amp;quot;c&amp;quot;)
.unpivot(index=&amp;quot;date&amp;quot;, variable_name=&amp;quot;variable&amp;quot;, value_name=&amp;quot;value&amp;quot;),
)
+pn.geom_area(pn.aes(&amp;quot;date&amp;quot;, &amp;quot;value&amp;quot;, fill=&amp;quot;variable&amp;quot;), color=&amp;quot;gray&amp;quot;, size=0.2, alpha=0.8)
+pn.scale_fill_brewer(type=&amp;quot;qual&amp;quot;, palette=&amp;quot;Set2&amp;quot;)
+pn.scale_x_date(date_labels=&amp;quot;%Y/%m&amp;quot;)
+pn.scale_y_continuous(breaks=range(0, 100, 1))
+pn.theme_minimal()
+pn.labs(x=&amp;quot;date&amp;quot;, y=&amp;quot;volatility&amp;quot;, title=&amp;quot;realized volatility&amp;quot;, subtitle=&amp;quot;decomposed into continuous component (c) and jump component (j)&amp;quot;)
+pn.theme(figure_size=(8, 3), dpi=200, legend_position=&amp;quot;right&amp;quot;)
)
&lt;/code&gt;&lt;/pre&gt;
&lt;/details&gt;
&lt;p&gt;&lt;img src="index_files/figure-commonmark/cell-12-output-1.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;cとjは必ずしも同じような動きをしていないのが面白いです。&lt;/p&gt;
&lt;p&gt;さて、ジャンプ部分jの値が一番大きい日は2024/7/11、次に大きい日は2024/4/29でした。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;shape: (288, 8)
┌────────────┬─────┬──────────┬──────────┬───────────┬──────────┬──────────┬──────────┐
│ date ┆ n ┆ rv ┆ bv ┆ tq ┆ z ┆ j ┆ c │
│ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- │
│ date ┆ u32 ┆ f64 ┆ f64 ┆ f64 ┆ f64 ┆ f64 ┆ f64 │
╞════════════╪═════╪══════════╪══════════╪═══════════╪══════════╪══════════╪══════════╡
│ 2024-07-11 ┆ 288 ┆ 2.826537 ┆ 1.161507 ┆ 26.76854 ┆ 4.341744 ┆ 1.66503 ┆ 1.161507 │
│ 2024-04-29 ┆ 276 ┆ 5.346737 ┆ 3.789614 ┆ 85.518574 ┆ 3.002976 ┆ 1.557123 ┆ 3.789614 │
│ 2024-09-27 ┆ 288 ┆ 2.879915 ┆ 1.608273 ┆ 19.819779 ┆ 4.57689 ┆ 1.271642 ┆ 1.608273 │
│ … ┆ … ┆ … ┆ … ┆ … ┆ … ┆ … ┆ … │
│ 2024-12-03 ┆ 288 ┆ 0.588244 ┆ 0.561047 ┆ 0.635917 ┆ 0.724247 ┆ 0.0 ┆ 0.588244 │
│ 2024-12-05 ┆ 288 ┆ 0.471903 ┆ 0.458031 ┆ 0.306132 ┆ 0.537139 ┆ 0.0 ┆ 0.471903 │
│ 2024-12-06 ┆ 288 ┆ 0.486899 ┆ 0.455146 ┆ 0.425198 ┆ 1.023669 ┆ 0.0 ┆ 0.486899 │
└────────────┴─────┴──────────┴──────────┴───────────┴──────────┴──────────┴──────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;この2日は何があったのでしょうか？&lt;/p&gt;
&lt;p&gt;2024/7/11のドル円チャートはこちらです。&lt;/p&gt;
&lt;p&gt;&lt;img src="index_files/figure-commonmark/cell-14-output-1.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;2024/4/29はこちらです。&lt;/p&gt;
&lt;p&gt;&lt;img src="index_files/figure-commonmark/cell-15-output-1.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;気付きましたか？この2日は&lt;a href="https://www.asahi.com/articles/ASSC80H31SC8ULFA003M.html" target="_blank" rel="noopener noreferrer"&gt;為替介入が行われた日&lt;/a&gt;なんですね。為替介入であれば当然、ジャンプのような非連続な価格変動が起こります。&lt;/p&gt;
&lt;p&gt;連続な過程ではなくジャンプ拡散過程でモデリングするのがより適切であること、そして一般にイレギュラーな要因で生まれるジャンプ部分のボラティリティを分離して把握することが大切なことが分かるよい例でした。&lt;/p&gt;
&lt;h2 id="おわりに"&gt;おわりに&lt;/h2&gt;
&lt;p&gt;RVの理論と実装を簡単に解説してみました。&lt;/p&gt;
&lt;p&gt;金融市場の解析でも機械学習や深層学習の話題が多いですが、数理ファイナンスのトピックや論文も知っておくといろいろ使えることが多いと思います。&lt;/p&gt;
&lt;h2 id="参考文献"&gt;参考文献&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Andersen, T. and Bollerslev, T. (1998). Answering the Skeptics: Yes,
Standard Volatility Models do Provide Accurate Forecasts.
&lt;em&gt;International Economic Review&lt;/em&gt;, 39(4), 885-905.&lt;/li&gt;
&lt;li&gt;Barndorff-Nielsen, O. E., and Shephard, N. (2004). Power and Bipower
Variation with Stochastic Volatility and Jumps, &lt;em&gt;Journal of Financial
Econometrics&lt;/em&gt;, 2, 1-48.&lt;/li&gt;
&lt;li&gt;Barndorff-Nielsen, O. E., and Shephard, N. (2006). Econometrics of
Testing for Jumps in Financial Economics Using Bipower Variation,
&lt;em&gt;Journal of Financial Econometrics&lt;/em&gt;, 4, 1-30.&lt;/li&gt;
&lt;li&gt;Hansen, P. R., and Lunde, A. (2005). Realized Variance and Market
Microstructure Noise. &lt;em&gt;Journal of Business &amp;amp; Economic Statistics&lt;/em&gt;, 24,
127-161.&lt;/li&gt;
&lt;li&gt;Liu, L. Y., Patton, A. J., and Sheppard K. (2015). Does anything beat
5-minute RV? A comparison of realized measures across multiple asset
classes. &lt;em&gt;Journal of Econometrics&lt;/em&gt;, 187(1), 293-311.&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="footnotes" role="doc-endnotes"&gt;
&lt;hr&gt;
&lt;ol&gt;
&lt;li id="fn:1"&gt;
&lt;p&gt;価格が観測される時点が等間隔でなくても同じ議論が適用できます。&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:2"&gt;
&lt;p&gt;なお、株のように昼休みや夜間に休場する商品では、昼休みや夜間を挟む収益率も含んで普通にRVを計算すると、RVの精度が低くなり真のボラティリティに対する一致性を持たなくなります。このような市場でもRVが真のボラティリティの一致推定量になるように修正した計算方法があります
(Hansen and Lunde (2005))。&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:3"&gt;
&lt;p&gt;ドリフトと拡散係数が時変の確率過程となっています。つまり、ドリフトと拡散係数が定数である幾何ブラウン運動よりも広い条件で成り立ちます。&amp;#160;&lt;a href="#fnref:3" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:4"&gt;
&lt;p&gt;確率微分方程式の計算ルールである「伊藤ルール」より計算できます。&amp;#160;&lt;a href="#fnref:4" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:5"&gt;
&lt;p&gt;5%や1%、0.1%などがよく用いられます。小さくするほどジャンプを厳しく判定することになります。&amp;#160;&lt;a href="#fnref:5" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:6"&gt;
&lt;p&gt;論文のタイトルからして（他の時間間隔やRV以外の他のボラティリティの推定量と比較して）“Does
anything beat 5-minute RV?”という名前です。&amp;#160;&lt;a href="#fnref:6" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</description></item><item><title>ラグナロクオンラインの露店取引ログを返すAPIを作った</title><link>https://suzunano.net/posts/ro-trade-log/</link><pubDate>Fri, 02 Aug 2024 00:00:00 +0900</pubDate><guid>https://suzunano.net/posts/ro-trade-log/</guid><description>&lt;h2 id="概要"&gt;概要&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://ragnarokonline.gungho.jp/" target="_blank" rel="noopener noreferrer"&gt;ラグナロクオンライン&lt;/a&gt;（RO）という、2002年リリース（！）の老舗のMMORPGがあります。ドット絵のかわいさが特徴的なMMOです。&lt;/p&gt;
&lt;p&gt;ROには露店システムというものがあります。プレイヤーはゲーム内でお店を開くことができ、自分が所有しているアイテムに自分で値段を設定して他のプレイヤーに売ることができます。&lt;/p&gt;
&lt;p&gt;同じアイテムにはだいたいどのプレイヤーも似たような値段を付けるようになり、これが相場として形成されるわけですが、相場は時期によって大きく変動します。露店が並んでいる街を歩き回って掘り出し物を探すのも楽しいですし&lt;sup id="fnref:1"&gt;&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref"&gt;1&lt;/a&gt;&lt;/sup&gt;、今後もっと高くなると考え、アイテムを安い時期に買って高い時期に売ることで差益を狙うというのもROの一つの醍醐味です。&lt;/p&gt;
&lt;p&gt;何のアイテムがいついくらで売れたという露店の取引履歴は&lt;a href="https://rotool.gungho.jp/item/" target="_blank" rel="noopener noreferrer"&gt;公式のツール&lt;/a&gt;で提供されています。このツールは各アイテムについて最新1000件の取引履歴を表示します。露店に出すときは、だいたいこのツールの履歴と他の露店が付けている値段を見ながら適当な値段を設定します。&lt;/p&gt;
&lt;p&gt;このデータ、データ分析屋としては貯めておいて長い時期で価格変動を見てみたくなります。というわけで、取引履歴を定期的にクローリングして貯めておき、GETすると過去の履歴を返すようなAPIを作りました。（自分用なので公開はしていません）&lt;/p&gt;
&lt;h2 id="技術構成"&gt;技術構成&lt;/h2&gt;
&lt;p&gt;構成図はこちらです。&lt;/p&gt;
&lt;img src="./architecture.drawio.svg" width="800px"&gt;
&lt;ul&gt;
&lt;li&gt;VPS
&lt;ul&gt;
&lt;li&gt;Pythonで定期的に公式ツールをクローリングし、JSONで保存してデータをCloud Storageにアップロード
&lt;ul&gt;
&lt;li&gt;負荷をかけないようにするため、クローリングには十分なスリープを入れている&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Google Cloud + Terraform
&lt;ul&gt;
&lt;li&gt;Cloud StorageにアップロードされたらCloud Functionsを起動してBigQueryテーブルに書き込む
&lt;ul&gt;
&lt;li&gt;BigQueryに直接書き込まずにCloud Storageを前段に置いているのは、クローリングしたデータの形式が変わっていてBigQueryへの書き込みでエラーになる場合にデータが消失しないようにするため&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Cloud Functions（認証付きHTTPエンドポイント） + FastAPI（Python）でAPIエンドポイントを用意
&lt;ul&gt;
&lt;li&gt;GETでリクエストしたらBigQueryをクエリする&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;クローリングはVPS (cron), データ基盤はBigQuery + Terraformというオレオレ定番パターンです。以前に作った&lt;a href="../nicolog/"&gt;ニコニコ動画の再生数推移のデータ基盤&lt;/a&gt;と同じパターンです。&lt;/p&gt;
&lt;p&gt;なお、公式ツールは常に最新の1000件のログを表示するため、前回のクローリングの取引ログと重複します&lt;sup id="fnref:2"&gt;&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref"&gt;2&lt;/a&gt;&lt;/sup&gt;。こういう場合、重複を許して全件放り込むBigQueryテーブルに加え、重複を除いたユニークな取引ログを入れるBigQueryテーブルを用意して、前者にappendされるたびにスキャンして後者を全件洗い替えるのが定番です&lt;sup id="fnref:3"&gt;&lt;a href="#fn:3" class="footnote-ref" role="doc-noteref"&gt;3&lt;/a&gt;&lt;/sup&gt;。ですが、自分一人しか使わないアプリケーションのためにスキャンする方がかえってコストがかかるので、重複したままのBigQueryテーブルだけ作り、クエリするときにSQLで重複を取り除くようにしています。&lt;/p&gt;
&lt;h2 id="使ってみる"&gt;使ってみる&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://rotool.gungho.jp/item/4131/0/" target="_blank" rel="noopener noreferrer"&gt;月夜花カード&lt;/a&gt;というアイテムのレコードをAPIから取得してみます。&lt;/p&gt;
&lt;p&gt;最初のレコードは、2024/7/18 18:16に1.3G Zeny（Zenyは通貨の名前。1G Zeny = 1000 M Zeny = 1000^2 K Zeny = 1000^3 Zeny）で1個取引が成立したというレコードです。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-json"&gt;# 最新の3件の一部のkeyのみ抜粋
[
{
&amp;quot;datetime&amp;quot;: &amp;quot;2024-07-18T18:16:00+09:00&amp;quot;,
&amp;quot;world&amp;quot;: &amp;quot;Noatun&amp;quot;,
&amp;quot;item_id&amp;quot;: &amp;quot;4131&amp;quot;,
&amp;quot;item_name&amp;quot;: &amp;quot;月夜花カード&amp;quot;,
&amp;quot;price&amp;quot;: 1300000000,
&amp;quot;count&amp;quot;: 1
},
{
&amp;quot;datetime&amp;quot;: &amp;quot;2024-07-10T19:38:00+09:00&amp;quot;,
&amp;quot;world&amp;quot;: &amp;quot;Noatun&amp;quot;,
&amp;quot;item_id&amp;quot;: &amp;quot;4131&amp;quot;,
&amp;quot;item_name&amp;quot;: &amp;quot;月夜花カード&amp;quot;,
&amp;quot;price&amp;quot;: 1000000000,
&amp;quot;count&amp;quot;: 1
},
{
&amp;quot;datetime&amp;quot;: &amp;quot;2024-06-29T15:43:00+09:00&amp;quot;,
&amp;quot;world&amp;quot;: &amp;quot;Noatun&amp;quot;,
&amp;quot;item_id&amp;quot;: &amp;quot;4131&amp;quot;,
&amp;quot;item_name&amp;quot;: &amp;quot;月夜花カード&amp;quot;,
&amp;quot;price&amp;quot;: 1000000000,
&amp;quot;count&amp;quot;: 1
}
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;全部プロットしてみるとこんな感じです。価格変動が激しいですね。&lt;/p&gt;
&lt;img src="./4131.png" width="600px"&gt;
&lt;p&gt;APIで整形済みのデータが得られるのも分析的に楽ですね。作ったはいいものの、何に使えるかは分かりませんが…。&lt;/p&gt;
&lt;p&gt;© Gravity Co., Ltd. &amp;amp; Lee MyoungJin(studio DTDS). All rights reserved.&lt;br&gt;
© GungHo Online Entertainment, Inc. All Rights Reserved.&lt;br&gt;
当コンテンツの再利用（再転載・配布など）は、禁止しています。&lt;/p&gt;
&lt;div class="footnotes" role="doc-endnotes"&gt;
&lt;hr&gt;
&lt;ol&gt;
&lt;li id="fn:1"&gt;
&lt;p&gt;プロンテラの露店街はROの華ですね。&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:2"&gt;
&lt;p&gt;例えば前のクローリングから新たに100件のログが増えていた場合、900件分のレコードが二重にカウントされることになります。&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:3"&gt;
&lt;p&gt;例えば: &lt;a href="https://dev.classmethod.jp/articles/bigquery-load-diff/" target="_blank" rel="noopener noreferrer"&gt;BigQuery にデータを差分ロード（UPSERT）する方法まとめ&lt;/a&gt;&amp;#160;&lt;a href="#fnref:3" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</description></item><item><title>読んだ: 「はじめての統計的因果推論」</title><link>https://suzunano.net/posts/hajimete-no-causal-inference/</link><pubDate>Tue, 30 Jul 2024 00:00:00 +0900</pubDate><guid>https://suzunano.net/posts/hajimete-no-causal-inference/</guid><description>&lt;p&gt;「はじめての統計的因果推論」（著: 林 岳彦）を読みました。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.amazon.co.jp/dp/4000058428" target="_blank" rel="noopener noreferrer"&gt;Amazonのリンク&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.iwanami.co.jp/book/b639904.html" target="_blank" rel="noopener noreferrer"&gt;出版社のリンク&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;最近出版された統計的因果推論の本で評価が高かったので読んでみました。統計的因果推論に興味のある非データ分析者や初めて学ぶデータ分析者はもちろん、統計的因果推論を多少知っていて理解を整理したいデータ分析者にも非常によい本だと思いました。ほとんど数式を用いずに本質を説明していたのが印象的でした。なお、具体的な実装方法を説明している本ではありません。&lt;/p&gt;
&lt;p&gt;いいなと思ったのは三点あります。&lt;/p&gt;
&lt;p&gt;まず、因果効果を推定するとはどういうことなのか、じっくりと丁寧に論理を積み重ねて、これでもかというくらいかみ砕いて説明している点です。ここは書籍の第1部に該当します。&lt;/p&gt;
&lt;p&gt;本で取り上げられている例を挙げます。いま、肥料を使用するとりんごの糖度が高くなるのかどうかを調べたいとします。肥料を使用したりんご（介入群）が数十個、使用していないりんご（対照群）が数十個あるとして、肥料を使用すると何度糖度が上がるか、このりんごからどうやって推定しましょうか？&lt;/p&gt;
&lt;p&gt;介入群のりんごと対照群のりんごについて、それぞれ糖度を平均して引き算すればよいと思いつくかもしれません。ですがこの数値は因果効果とは限りません。というのは、例えば介入群にはもともと糖度が高い品種のりんごが、対照群のりんごにはもともと糖度が低い品種のりんごが多く含まれる場合、単なる平均の差では、このもともとの品種による糖度の違いを因果効果に加えてしまっているため、本来の因果効果よりも過大に効果を推定するからです（セレクションバイアス）。&lt;/p&gt;
&lt;p&gt;品種に加え、天候などその他あらゆる要因の分布が介入群と対照群で等しいときに、肥料という処置による糖度への因果効果の推定値は、実際に観測された群間の糖度の平均の差となります。統計的因果推論の本質は、統計モデリングによって群間での要因の分布を揃える、あるいは揃えた状況を作り出すことです。実際にはすべての要因の分布を群間で揃える必要はなく、特定の要因（共変量）が揃っていれば因果効果を推定できます。何の要因を揃える必要があるのかを解き明かすために、因果ダイアグラムやバックドア基準を書籍中で説明しています。&lt;/p&gt;
&lt;p&gt;RCTは、あらゆる要因の分布を群間で揃えられるので最も強力な手法です。因果ダイアグラムの観点から見ると、共変量を無作為割付（コイントス）という単一の変数に絞ることでバックドア基準を達成する方法であり、潜在結果モデルの観点から見ると、本来観測できない反事実下での期待値を無作為割付によって観測値で代替できるようにする方法です。&lt;/p&gt;
&lt;p&gt;RCTが使えない場合に2群間で要因の分布を揃える方法として、層別化や重回帰分析、傾向スコアなどの各種手法を使用します。この書籍のおすすめポイントの二つ目は、第2部でこれらの各種手法の計算ロジックを暗算で計算できる簡単な例で説明していることです。各手法にはメリットとデメリットがありますし、適用するためにデータが満たす必要がある特徴もありますが（例えば、傾向スコア法では傾向スコアの分布が介入群と対照群で似ている必要があります（コモンサポート））、手計算レベルの例をなぞることで自然に理解できます。&lt;/p&gt;
&lt;p&gt;最後に第3部で、統計的因果推論で推定される因果効果とは何か、科学哲学的な観点からしっかり説明している点もよかったです。&lt;/p&gt;
&lt;p&gt;実験をよく設計しなければ、本来の処置の効果とは違う効果が因果効果の中に含まれてしまうことがあります（だから二重盲検法を使用するのですね）。また、特定の集団から取り出したサンプルで推定した因果効果を、その特定の集団を包含するより大きい集団の因果効果として扱っていいのかという問題もあります（外的妥当性）。&lt;/p&gt;
&lt;p&gt;これらは統計的因果推論の本では、特に実装系の本では触れられないこともありますが、統計的因果推論を実社会に適用する上でデータ分析者が考慮しなければならない非常に重要なポイントです。章を複数割いて説明しているところに著者の心意気を感じました。&lt;/p&gt;
&lt;p&gt;具体的な実装方法や理論を学びたい場合は例えば次の書籍に進むとよいのではないでしょうか。最初の二つの書籍は特に定番ですね。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.amazon.co.jp/dp/4297111179" target="_blank" rel="noopener noreferrer"&gt;効果検証入門〜正しい比較のための因果推論/計量経済学の基礎&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.amazon.co.jp/dp/4320112458" target="_blank" rel="noopener noreferrer"&gt;統計的因果推論の理論と実装&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.amazon.co.jp/dp/4274231232" target="_blank" rel="noopener noreferrer"&gt;因果推論: 基礎から機械学習・時系列解析・因果探索を用いた意思決定のアプローチ&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.amazon.co.jp/dp/4297134179" target="_blank" rel="noopener noreferrer"&gt;因果推論入門〜ミックステープ：基礎から現代的アプローチまで&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>読んだ: 「やりたいことが今すぐわかる 逆引きGit入門」</title><link>https://suzunano.net/posts/git-nyuumon/</link><pubDate>Mon, 01 Jul 2024 00:00:00 +0900</pubDate><guid>https://suzunano.net/posts/git-nyuumon/</guid><description>&lt;p&gt;「やりたいことが今すぐわかる 逆引きGit入門」（著: 高 見龍、訳: 鶴本 彰子）を読みました。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.amazon.co.jp/dp/4798059595" target="_blank" rel="noopener noreferrer"&gt;Amazonのリンク&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.shuwasystem.co.jp/book/9784798059594.html" target="_blank" rel="noopener noreferrer"&gt;出版社のリンク&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;数年前に一度読み、先日再び一部読み直しました。特に有名な本ではない気がしますが、Gitの入門書として良書だと思ったので感想を書きます。ちなみにわたしがGitを困らずに使えるようになったのはこの本のおかげです。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Gitの操作を一つずつ仕組みを踏まえて解説していく本&lt;/li&gt;
&lt;li&gt;コマンドラインに加え、GUIとしてSourcetreeの二つ扱っている&lt;/li&gt;
&lt;li&gt;著者は台湾の人っぽい
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/kaochenlong" target="_blank" rel="noopener noreferrer"&gt;https://github.com/kaochenlong&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;語りかけているような講義調の文体。著者の文章と訳者の訳が上手なのか頭にすっと入ってきた&lt;/li&gt;
&lt;li&gt;逆引きGit入門というタイトルだが、辞書感はあまりない（タイトルでちょっと損している気が…）
&lt;ul&gt;
&lt;li&gt;こういうときどうするかという事例集でありつつも、ハンズオン形式でGitの概念とコマンドを順を追って説明しているからかな？&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Gitのガベージコレクションや&lt;code&gt;.git&lt;/code&gt;ディレクトリの中身などのちょっと進んだ解説が面白かった
&lt;ul&gt;
&lt;li&gt;ある程度Gitを使えるようになってからもう一度読むとさらに勉強になる！&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;こういう人に合っていると思う（かつての自分）
&lt;ul&gt;
&lt;li&gt;Gitは何となく使えるけどググってばかり&lt;/li&gt;
&lt;li&gt;Gitってなんか怖い&lt;/li&gt;
&lt;li&gt;Gitの概念や仕組みからしっかり理解したい&lt;/li&gt;
&lt;li&gt;コマンドを使えるようになりたい&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="印象に残っている部分"&gt;印象に残っている部分&lt;/h2&gt;
&lt;p&gt;二つ紹介します。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;git add&lt;/code&gt;でステージングエリアに載せてから&lt;code&gt;git commit&lt;/code&gt;でコミットと、なぜ2回操作が必要なのか？という質問で、以下のように例えていたのが面白かったです。（4.3章）（以下の文章は本の抜粋ではなく、該当の文章をわたしが意訳したものです）&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;倉庫を一つ所有していて、倉庫の前にはちょっとした荷物置き場のスペースがあるとする。トラックで届く荷物を待っているとする。届いた荷物は一旦倉庫の前の空きスペース（ステージングエリア）に置いておく（&lt;code&gt;git add&lt;/code&gt;）。ある程度届いたら倉庫（リポジトリ）に移す（&lt;code&gt;git commit&lt;/code&gt;）。荷物が届くたびに毎回倉庫に移してもいいけど、そうすると倉庫に移した記録（コミットログ）が細々して後で見返すと分からなくなりがち。きりのいい単位で荷物をまとめて倉庫に移して記録を付けると分かりやすいよね。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;もう一つ、ブランチを切り替え忘れてコミットしてしまったときの対処法です。（11.1章）&lt;/p&gt;
&lt;p&gt;こういうコミットログを考えます。&lt;/p&gt;
&lt;p&gt;コミットIDがc004とc005のコミットはdevelopブランチに積むつもりがmainブランチにすでに積んでしまった！今のmainとdevelopを入れ替えたいというケースです。時々やるやつですね。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;git log --oneline
c005 (HEAD, main) foo
c004 piyo
c003 (develop) fuga
c002 hoge
c001 first commit
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;どうやって入れ替えましょうか？この本では以下の二つの方法が紹介されています。&lt;/p&gt;
&lt;p&gt;まずはdevelopを元々置きたかったc005（今のHEAD）に持ってきてから、mainブランチをc003まで持ってくる方法です。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;# git branch &amp;lt;branch-name&amp;gt; &amp;lt;commit-ID&amp;gt;でbranch-nameをcommit-IDに作る
# branch-nameが既に存在するときは-fオプションを使う
git branch -f develop c005
# この段階ではmainブランチをcheckoutしている
# 以下の2行の代わりにgit reset --hard c003でもいい
git switch develop
git branch -f main c003
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;もう一つ、mainとdevelopの名前を入れ替える方法もあります。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;# ブランチの名前を変える時に、そのブランチがHEADにいると名前を変えられないので
# c005にHEADを置く（detached HEAD）
# git checkout c005でもよい
git switch c005 --detach
git branch -m develop tmp
git branch -m main develop
git branch -m tmp main
# detached HEADを解消する
git switch main
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;変数xとyを入れ替えるときに、yをtmpに、xをyにして最後にtmpをxに変える方法がありますが、これと同じことですね。鮮やかな解法ですね。&lt;/p&gt;
&lt;p&gt;本の中で繰り返し述べられていますが、ブランチというのはコミットログのツリーの枝（例えばVSCodeの拡張機能であるGit Graphの線）全体を指すものではなく&lt;sup id="fnref:1"&gt;&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref"&gt;1&lt;/a&gt;&lt;/sup&gt;、特定のコミットに貼り付けた付箋のようなものです。&lt;/p&gt;
&lt;p&gt;最初の解法では&lt;code&gt;git branch &amp;lt;branch-name&amp;gt; &amp;lt;commit-ID&amp;gt;&lt;/code&gt;によって、付箋を剥がしては別のコミットに貼るイメージ、次の解法では&lt;code&gt;git branch -m&lt;/code&gt;によって付箋を貼り変えるイメージです。&lt;/p&gt;
&lt;p&gt;ブランチはツリーではなくコミットを指す付箋だという教えを実感できる解法が本の最後に出てきて、読んでいて感動しました。&lt;/p&gt;
&lt;p&gt;なお、本では触れられていませんが、developブランチにcherry-pickする方法もあります。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;git switch develop
git cherry-pick c004 c005
git switch main
git reset --hard c003
&lt;/code&gt;&lt;/pre&gt;
&lt;div class="footnotes" role="doc-endnotes"&gt;
&lt;hr&gt;
&lt;ol&gt;
&lt;li id="fn:1"&gt;
&lt;p&gt;じゃあよく見るコミットログのツリーの枝は何なのかですが、各コミットにはその親のコミット（≒一個前のコミット）のコミットIDが記録されています。それをつなげたのがよく見るツリーの枝です。&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</description></item><item><title>部屋の階数は家賃にどれだけ影響を与えるのか？</title><link>https://suzunano.net/posts/rent-by-floor/</link><pubDate>Fri, 24 May 2024 00:00:00 +0900</pubDate><guid>https://suzunano.net/posts/rent-by-floor/</guid><description>&lt;h2 id="はじめに"&gt;はじめに&lt;/h2&gt;
&lt;p&gt;昨年の確率的プログラミング言語アドベントカレンダーに出した記事（&lt;a href="../rent-modeling/"&gt;階層ベイズで東京23区のお部屋の家賃相場を推定する -
suzuna’s
memo&lt;/a&gt;）の続きです。（本記事を読む上では、この記事は読まなくて大丈夫です）&lt;/p&gt;
&lt;p&gt;前の記事では、まず賃貸物件の情報サイトであるSUUMOをスクレイピングすることで、約20万件の東京23区の賃貸物件の家賃データを収集しました。そのデータを用いて、東京23区の家賃相場を推定する階層ベイズモデルをR +
RStanで実装しました。なお、ここでいう家賃とは、毎月発生する家賃と管理費の合計を指します（以下、単に家賃と記載します）。&lt;/p&gt;
&lt;p&gt;このモデルの説明変数には最寄り駅、部屋の面積、築年数、駅からの徒歩分数を使用しました。そのため、部屋がある階数の情報を考慮できていませんでした。同じマンションでも部屋の階数が高いほど家賃は高くなります。1階や地下1階の部屋の家賃は安く設定されていることが多いですし、2階と10階では家賃にそれなりの開きがあります。また、最寄り駅によって、高層マンションの物件が多い駅と低層マンションの物件が多い駅があるため、階を考慮しなければ、前者の駅ほど割高に推定することになります。&lt;/p&gt;
&lt;p&gt;そこでこの記事では、先の東京23区の家賃相場のモデルの説明変数に部屋の階数を追加しました。これにより、階数が1階上がるごとに家賃がどの程度高くなるのかや、1階や地下階だとどの程度安くなるのかを解き明かしてみようと思います。また、築年数1年や、駅からの徒歩分数1分ごとに家賃がどの程度変化するかも合わせて示します。&lt;/p&gt;
&lt;p&gt;さらに、最寄り駅以外の全ての条件（説明変数）を揃えたとき、最寄り駅によってどの程度家賃相場が変わるかを見てみようと思います。&lt;/p&gt;
&lt;h2 id="結論"&gt;結論&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;築年数が1年増えるごとに家賃は1%下がる&lt;/li&gt;
&lt;li&gt;駅から徒歩1分増えるごとに家賃は0.7%下がる&lt;/li&gt;
&lt;li&gt;物件の階数が2階から1階上がるごとに家賃は1.2%上がる&lt;/li&gt;
&lt;li&gt;最上階かどうかは家賃に影響がない&lt;/li&gt;
&lt;li&gt;1階は2階と比べて家賃は3.6%、地下1階は2階と比べて家賃は6.6%下がる&lt;/li&gt;
&lt;li&gt;任意の最寄り駅、面積、築年数、駅からの徒歩分数、階数における物件の家賃相場を示すことができた
&lt;ul&gt;
&lt;li&gt;例:
最寄り駅が表参道の25m2、築5年、駅から徒歩5分、3階のマンションの家賃相場は15.9万円&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="環境"&gt;環境&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;R 4.3.1&lt;/li&gt;
&lt;li&gt;rstan 2.32.3&lt;/li&gt;
&lt;li&gt;bayesplot 1.10.0&lt;/li&gt;
&lt;li&gt;tidybayes 3.0.6&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="使うデータ"&gt;使うデータ&lt;/h2&gt;
&lt;p&gt;2023年11月にSUUMOからスクレイピングした東京23区の賃貸マンションの賃貸データを用います。&lt;/p&gt;
&lt;p&gt;用いたのは、東京23区の賃貸物件のうち、以下に該当する物件です。124354件の物件です。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;SUUMOのカテゴリが「賃貸マンション」の物件（アパートや一戸建てを除く）&lt;sup id="fnref:1"&gt;&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref"&gt;1&lt;/a&gt;&lt;/sup&gt;&lt;/li&gt;
&lt;li&gt;面積が15m2～100m2の物件&lt;sup id="fnref:2"&gt;&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref"&gt;2&lt;/a&gt;&lt;/sup&gt;&lt;/li&gt;
&lt;li&gt;マンションの高さが、地下階はないか地下1階まで、かつ地上階は15階以下の物件&lt;sup id="fnref:3"&gt;&lt;a href="#fn:3" class="footnote-ref" role="doc-noteref"&gt;3&lt;/a&gt;&lt;/sup&gt;&lt;/li&gt;
&lt;li&gt;築年数が40年以下の物件&lt;/li&gt;
&lt;li&gt;駅から徒歩（＝車やバスではない）かつ駅からの徒歩分数が20分以内の物件&lt;/li&gt;
&lt;li&gt;家賃+管理費が100万円以下の物件&lt;/li&gt;
&lt;li&gt;階数の情報がページに存在し、かつ建物の地下階の階数&amp;lt;=物件の階数&amp;lt;=地上階の階数である物件
&lt;ul&gt;
&lt;li&gt;SUUMOの誤記入なのか、3階建てのマンションなのに部屋が4階と書かれているようなことがまれにあるが、そういうものを除くということ&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;25m2の物件と150m2の物件だったり、駅から徒歩10分の物件と駅からバスで20分、バス停から徒歩5分の物件を同じ線形モデルで説明することは無理があります。150m2の物件やバスで20分に徒歩5分の物件は、数としては多くないので、使用しないことにしたということです。&lt;/p&gt;
&lt;p&gt;前処理の内容や可視化については前の記事で詳細に触れていますので興味があれば見てみてください。&lt;/p&gt;
&lt;h2 id="モデル"&gt;モデル&lt;/h2&gt;
&lt;p&gt;家賃相場は、物件の最寄り駅、面積、築年数、駅からの徒歩分数、部屋の階数で決まると考えます。ここでいう家賃相場とは、これらの条件（＝説明変数）なら平均的にはこのくらいの家賃になるという水準です（そのため、実際に観測される家賃は、この家賃相場の上下に分布します）。&lt;/p&gt;
&lt;p&gt;具体的には、以下のモデルで定式化します。&lt;/p&gt;
&lt;p&gt;物件$i(1, \dots, N)$の最寄り駅（SUUMOの物件ページで一番上に書いてある1番目の最寄り駅）を$sta[i] (1, \dots, S)$とします。このとき、物件の対数家賃の相場は$\mu_{i}$万円であると考えます。&lt;/p&gt;
&lt;p&gt;$$
\begin{align*}
\log{y_{i}} &amp;amp; \sim N(\mu_{i}, \sigma) \\\
\mu_{i} &amp;amp;= a_{sta[i]} + b_{sta[i]} \log{\mathrm{area}_{i}} \\\
&amp;amp;+ \beta_{\mathrm{age}} \mathrm{age}_{i} + \beta_{\mathrm{walk}}(\mathrm{walk}_{i} - 1) \\\
&amp;amp;+ \beta_{\mathrm{floor}} \max {(\mathrm{floor}_{i} - 2, 0)} \\\
&amp;amp;+ \beta_{\mathrm{isTop}} \mathrm{isTop}_{i} \\\
&amp;amp;+ \beta_{\mathrm{isGround}} \mathrm{isGround}_{i} \\\
&amp;amp;+ \beta_{\mathrm{isUnderground}} \mathrm{isUnderground}_{i} \\\
a_{sta[i]} &amp;amp; \sim N(a_{all}, \sigma_{a_{all}}) \\\
b_{sta[i]} &amp;amp; \sim N(b_{all}, \sigma_{b_{all}}) \\\
\end{align*}
$$&lt;/p&gt;
&lt;p&gt;ただし、物件$i$について、それぞれ以下の通りとします。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$y_{i}$: 家賃+管理費（万円）&lt;/li&gt;
&lt;li&gt;$\mathrm{area}_{i} (15 \leq \mathrm{area}_{i} \leq 100)$: 面積（m2）&lt;/li&gt;
&lt;li&gt;$\mathrm{age}_{i} (= 0, 1, \dots, 40)$: 築年数。新築は0年とする&lt;/li&gt;
&lt;li&gt;$\mathrm{walk}_{i} (= 1, 2, \dots, 20)$: 最寄り駅からの徒歩分数&lt;/li&gt;
&lt;li&gt;$\mathrm{floor}_{i} (= -1, 1, 2, \dots, 15)$: 物件の階数&lt;/li&gt;
&lt;li&gt;$\mathrm{isTop}_{i} (= 0, 1)$: その部屋が最上階なら1,
そうではないなら0&lt;/li&gt;
&lt;li&gt;$\mathrm{isGround}_{i} (= 0, 1)$: その部屋が1階なら1,
そうではないなら0&lt;/li&gt;
&lt;li&gt;$\mathrm{isUnderground}_{i} (= 0, 1)$: その部屋が地下1階なら1,
そうではないなら0&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;面積の対数と家賃の対数は線形の関係で、その切片と傾きは最寄り駅によって違うというモデルです。要するに最寄り駅によって家賃水準が変わってくるということです。同じ面積の部屋でも家賃の高い駅と安い駅がありますし、面積を大きくしたときに家賃の上がり幅が大きい駅と小さい駅があります。これを階層ベイズで表現します&lt;sup id="fnref:4"&gt;&lt;a href="#fn:4" class="footnote-ref" role="doc-noteref"&gt;4&lt;/a&gt;&lt;/sup&gt;。&lt;/p&gt;
&lt;p&gt;築年数が1年増えたり、最寄り駅から徒歩1分増えたり、部屋の階数が1階上がったりするごとに家賃が一定割合増減するという仮定を置きます。部屋が最上階なら追加で一定割合家賃が上がり、反対に1階や地下1階なら2階と比べて一定割合家賃が下がるとします（家探しをしたことがあれば何となく分かると思いますが、2階以上では階が上がるごとに一定割合家賃が上がっていく一方で、1階と地下1階はそれより大きい割合で家賃が安くなると思われるため、1階と地下1階は別のパラメータに分けました。ドメイン知識ですね）。これらの割合は、最寄り駅によらず一定とします。&lt;/p&gt;
&lt;p&gt;なお、SUUMOでは物件ごとに最寄り駅が最大3つ書かれていますが、このモデルでは最初に書かれている1番目の最寄り駅のみを使用しています。他の最寄り駅も考慮するとより精緻になりそうですが、これでも駅ごとの家賃の大まかな傾向はとらえられると思われます。&lt;/p&gt;
&lt;h2 id="stanの実装"&gt;Stanの実装&lt;/h2&gt;
&lt;p&gt;このモデルをStanのコードで書きます。事前分布は無情報事前分布です。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-stan"&gt;data {
int N; // 物件の数
vector[N] Y; // 家賃+管理費
vector[N] AREA; // 面積
int S; // 最寄り駅の数
int&amp;lt;lower=1, upper=S&amp;gt; STATION[N]; // 物件nの最寄り駅index
vector[N] AGE; // 物件nの築年数
vector[N] WALK; // 物件nの徒歩分数
vector[N] FLOOR; // 物件nの階数（ただし、1階や地下1階の場合は0）
vector[N] IS_TOP;
vector[N] IS_GROUND;
vector[N] IS_UNDERGROUND;
}
parameters {
real a0; // 面積の切片の全体平均
real b0; // 面積の傾きの全体平均
vector[S] a;
vector[S] b;
real&amp;lt;upper=0&amp;gt; age_b;
real&amp;lt;upper=0&amp;gt; walk_b;
real&amp;lt;lower=0&amp;gt; floor_b;
real&amp;lt;lower=0&amp;gt; floor_b_is_top;
real&amp;lt;upper=0&amp;gt; floor_b_is_ground;
real&amp;lt;upper=0&amp;gt; floor_b_is_underground;
real&amp;lt;lower=0&amp;gt; sigma_a;
real&amp;lt;lower=0&amp;gt; sigma_b;
real&amp;lt;lower=0&amp;gt; sigma;
}
model {
a ~ normal(a0, sigma_a);
b ~ normal(b0, sigma_b);
log(Y) ~ normal(
a[STATION] + b[STATION] .* log(AREA) +
age_b * AGE +
walk_b * (WALK - 1)+
floor_b * (FLOOR - 2)+
floor_b_is_top * IS_TOP +
floor_b_is_ground * IS_GROUND +
floor_b_is_underground * IS_UNDERGROUND,
sigma
);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;このStanコードをmodel.stanというファイル名で保存し、以下のコードでRStanでキックします。chains=4,
iter=5000, warmup=1000で約20時間かかりました。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-r"&gt;library(tidyverse)
library(rstan)
library(bayesplot)
library(tidybayes)
library(patchwork)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class="language-r"&gt;# 上はMCMCの並列化、下はstanコードが変わらない限り再コンパイルしない
options(mc.cores=parallel::detectCores())
rstan::rstan_options(auto_write=TRUE)
# Stanコードのコンパイル
mod &amp;lt;- rstan::stan_model(&amp;quot;model.stan&amp;quot;)
# MCMCの実行
# dataはstanコードのdataブロックのN, Y, ...をlist(N=hoge, Y=fuga, ...)のように持つ
fit &amp;lt;- rstan::sampling(
mod,
data=data,
chains=4, iter=5000, warmup=1000, thin=1, refresh=10, seed=1234
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;MCMCが収束していることをトレースプロットやRhatなどでチェックしましたが、結果は割愛します。（具体的な実装は前回の記事をご参照ください）&lt;/p&gt;
&lt;h2 id="結果"&gt;結果&lt;/h2&gt;
&lt;p&gt;Stanのパラメータ推定結果です。（一部のパラメータのみ抜粋）&lt;/p&gt;
&lt;details&gt;
&lt;summary&gt;Code&lt;/summary&gt;
&lt;pre&gt;&lt;code class="language-r"&gt;print(
fit,
pars=c(
&amp;quot;a0&amp;quot;, &amp;quot;b0&amp;quot;, &amp;quot;age_b&amp;quot;, &amp;quot;walk_b&amp;quot;,
&amp;quot;floor_b&amp;quot;, &amp;quot;floor_b_is_top&amp;quot;, &amp;quot;floor_b_is_ground&amp;quot;, &amp;quot;floor_b_is_underground&amp;quot;,
&amp;quot;sigma_a&amp;quot;, &amp;quot;sigma_b&amp;quot;, &amp;quot;sigma&amp;quot;
),
digits_summary=3
)
&lt;/code&gt;&lt;/pre&gt;
&lt;/details&gt;
&lt;pre&gt;&lt;code&gt;#&amp;gt; Inference for Stan model: anon_model.
#&amp;gt; 4 chains, each with iter=5000; warmup=1000; thin=1;
#&amp;gt; post-warmup draws per chain=4000, total post-warmup draws=16000.
#&amp;gt;
#&amp;gt; mean se_mean sd 2.5% 25% 50% 75% 97.5%
#&amp;gt; a0 -0.116 0 0.011 -0.139 -0.124 -0.116 -0.108 -0.094
#&amp;gt; b0 0.803 0 0.005 0.793 0.799 0.803 0.806 0.812
#&amp;gt; age_b -0.011 0 0.000 -0.011 -0.011 -0.011 -0.011 -0.010
#&amp;gt; walk_b -0.007 0 0.000 -0.007 -0.007 -0.007 -0.007 -0.007
#&amp;gt; floor_b 0.012 0 0.000 0.012 0.012 0.012 0.012 0.012
#&amp;gt; floor_b_is_top 0.000 0 0.000 0.000 0.000 0.000 0.000 0.000
#&amp;gt; floor_b_is_ground -0.036 0 0.001 -0.038 -0.037 -0.036 -0.036 -0.034
#&amp;gt; floor_b_is_underground -0.068 0 0.006 -0.080 -0.072 -0.068 -0.064 -0.056
#&amp;gt; sigma_a 0.219 0 0.009 0.203 0.213 0.219 0.225 0.237
#&amp;gt; sigma_b 0.099 0 0.004 0.092 0.097 0.099 0.101 0.106
#&amp;gt; sigma 0.110 0 0.000 0.110 0.110 0.110 0.111 0.111
#&amp;gt; n_eff Rhat
#&amp;gt; a0 23848 1
#&amp;gt; b0 24227 1
#&amp;gt; age_b 16923 1
#&amp;gt; walk_b 33368 1
#&amp;gt; floor_b 33061 1
#&amp;gt; floor_b_is_top 23531 1
#&amp;gt; floor_b_is_ground 35017 1
#&amp;gt; floor_b_is_underground 34735 1
#&amp;gt; sigma_a 16979 1
#&amp;gt; sigma_b 25626 1
#&amp;gt; sigma 16128 1
#&amp;gt;
#&amp;gt; Samples were drawn using NUTS(diag_e) at Thu Apr 4 16:17:37 2024.
#&amp;gt; For each parameter, n_eff is a crude measure of effective sample size,
#&amp;gt; and Rhat is the potential scale reduction factor on split chains (at
#&amp;gt; convergence, Rhat=1).
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id="築年数効果"&gt;築年数効果&lt;/h3&gt;
&lt;p&gt;以下、点推定値としてmedianを採用します。$\beta_{\mathrm{age}}$ =
-0.011でした。これは、築年数が1年増えるごとに、家賃の対数が0.011小さくなることを意味します。&lt;/p&gt;
&lt;p&gt;築年数1年につき家賃の対数が0.011小さくなると言われてもよく分からないので、家賃が何%小さくなるのかが知りたいですね。これは、$\mathrm{age}_{i} = 0, \dots, 40$としたときの$\exp (\beta_{\mathrm{age}} \mathrm{age}_{i})$の事後中央値と95%ベイズ信用区間を求めればよいです。&lt;/p&gt;
&lt;p&gt;medianは事後分布の中央値、upperとlowerは95%ベイズ信用区間の上限と下限です。&lt;/p&gt;
&lt;details&gt;
&lt;summary&gt;Code&lt;/summary&gt;
&lt;pre&gt;&lt;code class="language-r"&gt;age_b &amp;lt;- tidy_draws |&amp;gt;
pull(age_b)
res_age &amp;lt;- 0:40 |&amp;gt;
map_dfr(\(age) {
samples &amp;lt;- exp(age_b * age)
tibble::tibble(
age=age,
median=quantile(samples, 0.5),
lower=quantile(samples, 0.025),
upper=quantile(samples, 0.975)
)
})
# きりのいいageだけ表示する
res_age |&amp;gt;
filter(age %in% c(0:5, seq(5, 40, 5))) |&amp;gt;
print(n=15)
&lt;/code&gt;&lt;/pre&gt;
&lt;/details&gt;
&lt;pre&gt;&lt;code&gt;#&amp;gt; # A tibble: 13 × 4
#&amp;gt; age median lower upper
#&amp;gt; &amp;lt;int&amp;gt; &amp;lt;dbl&amp;gt; &amp;lt;dbl&amp;gt; &amp;lt;dbl&amp;gt;
#&amp;gt; 1 0 1 1 1
#&amp;gt; 2 1 0.990 0.989 0.990
#&amp;gt; 3 2 0.979 0.979 0.979
#&amp;gt; 4 3 0.969 0.969 0.969
#&amp;gt; 5 4 0.959 0.959 0.959
#&amp;gt; 6 5 0.949 0.948 0.949
#&amp;gt; 7 10 0.900 0.900 0.901
#&amp;gt; 8 15 0.854 0.853 0.855
#&amp;gt; 9 20 0.810 0.809 0.811
#&amp;gt; 10 25 0.769 0.768 0.770
#&amp;gt; 11 30 0.729 0.728 0.730
#&amp;gt; 12 35 0.692 0.691 0.693
#&amp;gt; 13 40 0.656 0.655 0.658
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;medianの列のとおり、&lt;strong&gt;築年数が1年増えるごとに家賃は1%下がります&lt;/strong&gt;。覚えやすいですね。&lt;/p&gt;
&lt;p&gt;例えば築20年の物件は、新築の物件と比較して19%（≒1-0.99^20）下がります。&lt;/p&gt;
&lt;h3 id="徒歩分数効果"&gt;徒歩分数効果&lt;/h3&gt;
&lt;details&gt;
&lt;summary&gt;Code&lt;/summary&gt;
&lt;pre&gt;&lt;code class="language-r"&gt;walk_b &amp;lt;- fit |&amp;gt;
tidybayes::spread_draws(walk_b) |&amp;gt;
pull(walk_b)
res_walk &amp;lt;- 1:20 |&amp;gt;
map_dfr(\(walk) {
samples &amp;lt;- exp(walk_b * (walk - 1))
tibble::tibble(
walk=walk,
median=quantile(samples, 0.5),
lower=quantile(samples, 0.025),
upper=quantile(samples, 0.975)
)
})
res_walk |&amp;gt;
filter(walk %in% c(1, 2, 3, 5, 10, 15, 20)) |&amp;gt;
print()
&lt;/code&gt;&lt;/pre&gt;
&lt;/details&gt;
&lt;pre&gt;&lt;code&gt;#&amp;gt; # A tibble: 7 × 4
#&amp;gt; walk median lower upper
#&amp;gt; &amp;lt;int&amp;gt; &amp;lt;dbl&amp;gt; &amp;lt;dbl&amp;gt; &amp;lt;dbl&amp;gt;
#&amp;gt; 1 1 1 1 1
#&amp;gt; 2 2 0.993 0.993 0.993
#&amp;gt; 3 3 0.986 0.986 0.986
#&amp;gt; 4 5 0.972 0.972 0.973
#&amp;gt; 5 10 0.939 0.937 0.941
#&amp;gt; 6 15 0.907 0.904 0.909
#&amp;gt; 7 20 0.876 0.872 0.879
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;同様に、&lt;strong&gt;駅から徒歩1分増えるごとに家賃は0.7%下がります&lt;/strong&gt;。例えば駅から徒歩10分の物件は徒歩1分の物件と比べて6.1%（≒1-0.993^9）下がります。駅から遠くても家賃はあまり下がりませんね。&lt;/p&gt;
&lt;p&gt;徒歩分数1分あたりの家賃の変化量は全ての駅で一定としていますが、地上駅やターミナル駅では駅に近すぎると電車や駅周辺の騒音の影響で家賃が下がりそうな気もします。&lt;/p&gt;
&lt;h3 id="階数効果"&gt;階数効果&lt;/h3&gt;
&lt;details&gt;
&lt;summary&gt;Code&lt;/summary&gt;
&lt;pre&gt;&lt;code class="language-r"&gt;options(pillar.sigfig=4)
floor_b &amp;lt;- fit |&amp;gt;
tidybayes::spread_draws(floor_b) |&amp;gt;
pull(floor_b)
res_floor &amp;lt;- 2:15 |&amp;gt;
map_dfr(\(floors) {
samples &amp;lt;- exp(floor_b * max(floors - 2, 0))
tibble::tibble(
floor=floors,
median=quantile(samples, 0.5),
lower=quantile(samples, 0.025),
upper=quantile(samples, 0.975)
)
})
res_floor |&amp;gt;
print(digits=5)
&lt;/code&gt;&lt;/pre&gt;
&lt;/details&gt;
&lt;pre&gt;&lt;code&gt;#&amp;gt; # A tibble: 14 × 4
#&amp;gt; floor median lower upper
#&amp;gt; &amp;lt;int&amp;gt; &amp;lt;dbl&amp;gt; &amp;lt;dbl&amp;gt; &amp;lt;dbl&amp;gt;
#&amp;gt; 1 2 1 1 1
#&amp;gt; 2 3 1.012 1.012 1.012
#&amp;gt; 3 4 1.024 1.023 1.024
#&amp;gt; 4 5 1.036 1.035 1.037
#&amp;gt; 5 6 1.048 1.047 1.049
#&amp;gt; 6 7 1.061 1.059 1.062
#&amp;gt; 7 8 1.073 1.072 1.075
#&amp;gt; 8 9 1.086 1.084 1.088
#&amp;gt; 9 10 1.099 1.097 1.101
#&amp;gt; 10 11 1.112 1.109 1.115
#&amp;gt; 11 12 1.125 1.122 1.128
#&amp;gt; 12 13 1.139 1.135 1.142
#&amp;gt; 13 14 1.152 1.148 1.156
#&amp;gt; 14 15 1.166 1.162 1.170
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;物件の階数が2階から上に1階上がるごとに家賃が1.2%上がる&lt;/strong&gt;ことが分かりました。&lt;/p&gt;
&lt;h3 id="最上階効果1階効果地下1階効果"&gt;最上階効果、1階効果、地下1階効果&lt;/h3&gt;
&lt;details&gt;
&lt;summary&gt;Code&lt;/summary&gt;
&lt;pre&gt;&lt;code class="language-r"&gt;fit |&amp;gt;
tidybayes::spread_draws(
floor_b_is_top, floor_b_is_ground, floor_b_is_underground
) |&amp;gt;
mutate(
exp_is_top=exp(floor_b_is_top),
exp_is_ground=exp(floor_b_is_ground),
exp_is_underground=exp(floor_b_is_underground)
) |&amp;gt;
tidybayes::median_qi(
exp_is_top, exp_is_ground, exp_is_underground, .width=0.95
)
&lt;/code&gt;&lt;/pre&gt;
&lt;/details&gt;
&lt;pre&gt;&lt;code&gt;#&amp;gt; exp_is_top exp_is_top.lower exp_is_top.upper exp_is_ground
#&amp;gt; 1 1.000026 1.000001 1.000139 0.9644208
#&amp;gt; exp_is_ground.lower exp_is_ground.upper exp_is_underground
#&amp;gt; 1 0.9625942 0.9662279 0.9338659
#&amp;gt; exp_is_underground.lower exp_is_underground.upper .width .point .interval
#&amp;gt; 1 0.9228108 0.945169 0.95 median qi
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;以下のことが分かります。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;最上階であることは、家賃を全く押し上げない。&lt;/strong&gt;（exp_is_top）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;1階の物件は、2階の物件と比べて家賃が3.6%下がる。&lt;/strong&gt;（exp_is_ground）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;地下1階の物件は、2階の物件と比べて家賃が6.6%下がる&lt;sup id="fnref:5"&gt;&lt;a href="#fn:5" class="footnote-ref" role="doc-noteref"&gt;5&lt;/a&gt;&lt;/sup&gt;。&lt;/strong&gt;（exp_is_underground）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;階数効果と合わせて考えると、&lt;strong&gt;例えば地上4階地下1階建てのマンションで2階が家賃10万円なら、3階は10.12万円、4階は10.24万円、1階は9.64万円、地下1階は9.34万円&lt;/strong&gt;くらいになるということです。だいぶ妥当な感じの結果ですね。&lt;/p&gt;
&lt;p&gt;2階から3階は1.2%上がる一方、2階から1階は3.6%、2階から地下1階は6.6%下がることから、やはり1階や地下1階の物件の家賃はディスカウントされているということが分かりました。&lt;/p&gt;
&lt;p&gt;最上階だからといって、階数効果（1階につき1.2%）以上に追加で家賃を押し上げることはないというのがちょっと意外でした。ですが、新築のマンションの各部屋の家賃（新築は完成時に全階の物件が一斉に募集がかかる）を見たことがあるのですが、確かに最上階だからといって家賃が高くなることはないような気もしました。&lt;/p&gt;
&lt;h3 id="最寄り駅効果"&gt;最寄り駅効果&lt;/h3&gt;
&lt;p&gt;最寄り駅以外の条件を固定して、最寄り駅ごとに家賃相場がどの程度異なるかを見ることができます。25m2、築5年、駅から徒歩5分、3階の賃貸マンションという条件で、最寄り駅だけ変えてみましょう&lt;sup id="fnref:6"&gt;&lt;a href="#fn:6" class="footnote-ref" role="doc-noteref"&gt;6&lt;/a&gt;&lt;/sup&gt;。ちなみに25m2というのは一人暮らし用の物件でよくみられる面積です。&lt;/p&gt;
&lt;p&gt;まずは京王線沿いの各駅です。&lt;/p&gt;
&lt;details&gt;
&lt;summary&gt;Code&lt;/summary&gt;
&lt;pre&gt;&lt;code class="language-r"&gt;# 駅名とモデルに投入したindexのマッピング
sta_chr_idx_table &amp;lt;- df_mod |&amp;gt;
select(moyorieki_1_station, moyorieki_1_station_index) |&amp;gt;
distinct(moyorieki_1_station, .keep_all=TRUE)
# 駅名があればそのindex, なければNA_integer_を返す
station_to_idx &amp;lt;- function(station_name) {
chr &amp;lt;- sta_chr_idx_table$moyorieki_1_station
idx &amp;lt;- sta_chr_idx_table$moyorieki_1_station_index
if (length(idx[which(chr==station_name)]) == 0) {
return(NA_integer_)
} else {
return(idx[which(chr==station_name)])
}
}
tidy_draws_by_idx &amp;lt;- tidybayes::spread_draws(fit, a[idx], b[idx], age_b, walk_b, floor_b, floor_b_is_top, floor_b_is_ground, floor_b_is_underground, sigma_a, sigma_b)
stations &amp;lt;- c(
&amp;quot;新宿駅&amp;quot;, &amp;quot;初台駅&amp;quot;, &amp;quot;幡ヶ谷駅&amp;quot;, &amp;quot;笹塚駅&amp;quot;, &amp;quot;代田橋駅&amp;quot;, &amp;quot;明大前駅&amp;quot;, &amp;quot;下高井戸駅&amp;quot;, &amp;quot;桜上水駅&amp;quot;, &amp;quot;上北沢駅&amp;quot;, &amp;quot;八幡山駅&amp;quot;, &amp;quot;芦花公園駅&amp;quot;, &amp;quot;千歳烏山駅&amp;quot;
)
# factor型で駅の路線順に並べる
stations_fct &amp;lt;- forcats::fct_relevel(as.factor(stations), stations)
# 見る駅名のindex（stanのa[s]やb[s]のs）
idxs &amp;lt;- map_int(stations, station_to_idx)
area &amp;lt;- 25
age &amp;lt;- 5
walk &amp;lt;- 5
floor &amp;lt;- 3
is_top &amp;lt;- 0
p1 &amp;lt;- tidy_draws_by_idx |&amp;gt;
filter(idx %in% idxs) |&amp;gt;
# 駅のindexではなく駅名をプロットに付けるためにindexと駅名のテーブルをjoinする
left_join(
df_mod |&amp;gt;
filter(moyorieki_1_station %in% stations) |&amp;gt;
distinct(moyorieki_1_station, .keep_all=TRUE) |&amp;gt;
select(moyorieki_1_station, moyorieki_1_station_index) |&amp;gt;
rename(station=moyorieki_1_station, idx=moyorieki_1_station_index) |&amp;gt;
mutate(station=forcats::fct_relevel(station, stations)),
by=&amp;quot;idx&amp;quot;
) |&amp;gt;
mutate(
mu_exp=exp(
a+b*log(area)+age_b*age+walk_b*(walk-1)+
floor_b*max(floor-2, 0)+
floor_b_is_top*is_top+
floor_b_is_ground*as.integer(floor == 1L)+
floor_b_is_underground*as.integer(floor == -1L)
)
) |&amp;gt;
ggplot(aes(mu_exp, station))+
theme_light()+
tidybayes::stat_pointinterval(point_interval=tidybayes::median_qi, .width=0.95)+
scale_x_continuous(breaks=0:20)+
theme(axis.title.y=element_blank())+
labs(
title=&amp;quot;exp(mu_i) (25m2, 築5年, 徒歩5分, 3階)&amp;quot;,
subtitle=&amp;quot;point: estimated (median), bar: 95% bayesian CI&amp;quot;,
x=&amp;quot;exp(mu_i) (万円)&amp;quot;,
y=&amp;quot;station&amp;quot;
)
p2 &amp;lt;- df_mod |&amp;gt;
filter(moyorieki_1_station %in% stations) |&amp;gt;
count(moyorieki_1_station, moyorieki_1_station_index, name=&amp;quot;n&amp;quot;) |&amp;gt;
rename(station=moyorieki_1_station, idx=moyorieki_1_station_index) |&amp;gt;
mutate(station=forcats::fct_relevel(station, stations)) |&amp;gt;
arrange(station) |&amp;gt;
ggplot(aes(station, n))+
theme_light()+
geom_bar(stat=&amp;quot;identity&amp;quot;, color=&amp;quot;black&amp;quot;, fill=&amp;quot;gray&amp;quot;, alpha=0.6)+
scale_y_continuous(breaks=seq(0, 2000, 500), minor_breaks=seq(0, 2000, 100))+
geom_text(aes(label=n, y=100))+
theme(axis.title.y=element_blank())+
coord_flip()+
labs(
title=&amp;quot;（参考）物件数&amp;quot;
)
patchwork::wrap_plots(p1, p2, ncol=2, widths=c(3, 2))
&lt;/code&gt;&lt;/pre&gt;
&lt;/details&gt;
&lt;img src="images/image1-1.png" style="width:80.0%" /&gt;
&lt;p&gt;左のプロットは最寄り駅と上の条件での家賃相場（万円）です。真ん中の点が推定値、左右の棒は95%ベイズ信用区間です。右のプロットはSUUMOにその最寄り駅の物件が何件あったかを示します。例えば、25m2、築5年、駅から徒歩5分、3階のマンションの家賃相場は、最寄り駅が新宿だと14.3万円、初台だと12.1万円ということを示します。ちなみに、例えば築10年だとこの結果に0.95（≒1-0.99^(10-5)）をかけたものになります。&lt;/p&gt;
&lt;p&gt;明大前が下高井戸と代田橋より少し高く、また千歳烏山が芦花公園より少し高いことが面白いですね。明大前と千歳烏山は特急～各駅の全ての列車が止まること、明大前は京王井の頭線（渋谷～吉祥寺）も通ることが理由でしょうか。桜上水は新宿まで10分と近く、特急以外が止まります。閑静で住みやすい街ですが比較的お手頃な家賃で、住むにはよさそうですね。&lt;/p&gt;
&lt;p&gt;次に同じ条件で小田急線沿いを見てみます。京王線と同じく新宿が始発で、京王線の南側を走る路線です。&lt;/p&gt;
&lt;details&gt;
&lt;summary&gt;Code&lt;/summary&gt;
&lt;pre&gt;&lt;code class="language-r"&gt;stations &amp;lt;- c(
&amp;quot;新宿駅&amp;quot;, &amp;quot;南新宿駅&amp;quot;, &amp;quot;参宮橋駅&amp;quot;, &amp;quot;代々木八幡駅&amp;quot;, &amp;quot;代々木上原駅&amp;quot;, &amp;quot;東北沢駅&amp;quot;, &amp;quot;下北沢駅&amp;quot;, &amp;quot;世田谷代田駅&amp;quot;, &amp;quot;梅ヶ丘駅&amp;quot;, &amp;quot;豪徳寺駅&amp;quot;, &amp;quot;経堂駅&amp;quot;, &amp;quot;千歳船橋駅&amp;quot;, &amp;quot;祖師ヶ谷大蔵駅&amp;quot;, &amp;quot;成城学園前駅&amp;quot;
)
# factor型で駅の路線順に並べる
stations_fct &amp;lt;- forcats::fct_relevel(as.factor(stations), stations)
# 見る駅名のindex（stanのa[s]やb[s]のs）
idxs &amp;lt;- map_int(stations, station_to_idx)
area &amp;lt;- 25
age &amp;lt;- 5
walk &amp;lt;- 5
floor &amp;lt;- 3
is_top &amp;lt;- 0
p1 &amp;lt;- tidy_draws_by_idx |&amp;gt;
filter(idx %in% idxs) |&amp;gt;
# 駅のindexではなく駅名をプロットに付けるためにindexと駅名のテーブルをjoinする
left_join(
df_mod |&amp;gt;
filter(moyorieki_1_station %in% stations) |&amp;gt;
distinct(moyorieki_1_station, .keep_all=TRUE) |&amp;gt;
select(moyorieki_1_station, moyorieki_1_station_index) |&amp;gt;
rename(station=moyorieki_1_station, idx=moyorieki_1_station_index) |&amp;gt;
mutate(station=forcats::fct_relevel(station, stations)),
by=&amp;quot;idx&amp;quot;
) |&amp;gt;
mutate(
mu_exp=exp(
a+b*log(area)+age_b*age+walk_b*(walk-1)+
floor_b*max(floor-2, 0)+
floor_b_is_top*is_top+
floor_b_is_ground*as.integer(floor == 1L)+
floor_b_is_underground*as.integer(floor == -1L)
)
) |&amp;gt;
ggplot(aes(mu_exp, station))+
theme_light()+
tidybayes::stat_pointinterval(point_interval=tidybayes::median_qi, .width=0.95)+
scale_x_continuous(breaks=0:20)+
theme(axis.title.y=element_blank())+
labs(
title=&amp;quot;exp(mu_i) (25m2, 築5年, 徒歩5分, 3階)&amp;quot;,
subtitle=&amp;quot;point: estimated (median), bar: 95% bayesian CI&amp;quot;,
x=&amp;quot;exp(mu_i) (万円)&amp;quot;,
y=&amp;quot;station&amp;quot;
)
p2 &amp;lt;- df_mod |&amp;gt;
filter(moyorieki_1_station %in% stations) |&amp;gt;
count(moyorieki_1_station, moyorieki_1_station_index, name=&amp;quot;n&amp;quot;) |&amp;gt;
rename(station=moyorieki_1_station, idx=moyorieki_1_station_index) |&amp;gt;
mutate(station=forcats::fct_relevel(station, stations)) |&amp;gt;
arrange(station) |&amp;gt;
ggplot(aes(station, n))+
theme_light()+
geom_bar(stat=&amp;quot;identity&amp;quot;, color=&amp;quot;black&amp;quot;, fill=&amp;quot;gray&amp;quot;, alpha=0.6)+
scale_y_continuous(breaks=seq(0, 2000, 500), minor_breaks=seq(0, 2000, 100))+
geom_text(aes(label=n, y=100))+
theme(axis.title.y=element_blank())+
coord_flip()+
labs(
title=&amp;quot;（参考）物件数&amp;quot;
)
patchwork::wrap_plots(p1, p2, ncol=2, widths=c(3, 2))
&lt;/code&gt;&lt;/pre&gt;
&lt;/details&gt;
&lt;img src="images/image2-1.png" style="width:80.0%" /&gt;
&lt;p&gt;代々木上原まで、下北沢まで、成城学園前までで分かれていますね。経堂～梅ヶ丘も桜上水と同じく新宿から10分強ですし、静かな住みやすい街でよいのではないでしょうか。経堂は快速急行以外が止まるので便利ですね。&lt;/p&gt;
&lt;p&gt;最後に東京メトロの千代田線沿い（代々木上原～赤坂）を見てみます。国会議事堂前～大手町は物件がほとんどないので省略します。&lt;/p&gt;
&lt;details&gt;
&lt;summary&gt;Code&lt;/summary&gt;
&lt;pre&gt;&lt;code class="language-r"&gt;stations &amp;lt;- c(
&amp;quot;代々木上原駅&amp;quot;, &amp;quot;代々木公園駅&amp;quot;, &amp;quot;明治神宮前駅&amp;quot;, &amp;quot;表参道駅&amp;quot;, &amp;quot;乃木坂駅&amp;quot;, &amp;quot;赤坂駅&amp;quot;
)
# factor型で駅の路線順に並べる
stations_fct &amp;lt;- forcats::fct_relevel(as.factor(stations), stations)
# 見る駅名のindex（stanのa[s]やb[s]のs）
idxs &amp;lt;- map_int(stations, station_to_idx)
area &amp;lt;- 25
age &amp;lt;- 5
walk &amp;lt;- 5
floor &amp;lt;- 3
is_top &amp;lt;- 0
p1 &amp;lt;- tidy_draws_by_idx |&amp;gt;
filter(idx %in% idxs) |&amp;gt;
# 駅のindexではなく駅名をプロットに付けるためにindexと駅名のテーブルをjoinする
left_join(
df_mod |&amp;gt;
filter(moyorieki_1_station %in% stations) |&amp;gt;
distinct(moyorieki_1_station, .keep_all=TRUE) |&amp;gt;
select(moyorieki_1_station, moyorieki_1_station_index) |&amp;gt;
rename(station=moyorieki_1_station, idx=moyorieki_1_station_index) |&amp;gt;
mutate(station=forcats::fct_relevel(station, stations)),
by=&amp;quot;idx&amp;quot;
) |&amp;gt;
mutate(
mu_exp=exp(
a+b*log(area)+age_b*age+walk_b*(walk-1)+
floor_b*max(floor-2, 0)+
floor_b_is_top*is_top+
floor_b_is_ground*as.integer(floor == 1L)+
floor_b_is_underground*as.integer(floor == -1L)
)
) |&amp;gt;
ggplot(aes(mu_exp, station))+
theme_light()+
tidybayes::stat_pointinterval(point_interval=tidybayes::median_qi, .width=0.95)+
scale_x_continuous(breaks=0:20)+
theme(axis.title.y=element_blank())+
labs(
title=&amp;quot;exp(mu_i) (25m2, 築5年, 徒歩5分, 3階)&amp;quot;,
subtitle=&amp;quot;point: estimated (median), bar: 95% bayesian CI&amp;quot;,
x=&amp;quot;exp(mu_i) (万円)&amp;quot;,
y=&amp;quot;station&amp;quot;
)
p2 &amp;lt;- df_mod |&amp;gt;
filter(moyorieki_1_station %in% stations) |&amp;gt;
count(moyorieki_1_station, moyorieki_1_station_index, name=&amp;quot;n&amp;quot;) |&amp;gt;
rename(station=moyorieki_1_station, idx=moyorieki_1_station_index) |&amp;gt;
mutate(station=forcats::fct_relevel(station, stations)) |&amp;gt;
arrange(station) |&amp;gt;
ggplot(aes(station, n))+
theme_light()+
geom_bar(stat=&amp;quot;identity&amp;quot;, color=&amp;quot;black&amp;quot;, fill=&amp;quot;gray&amp;quot;, alpha=0.6)+
scale_y_continuous(breaks=seq(0, 2000, 500), minor_breaks=seq(0, 2000, 100))+
geom_text(aes(label=n, y=100))+
theme(axis.title.y=element_blank())+
coord_flip()+
labs(
title=&amp;quot;（参考）物件数&amp;quot;
)
patchwork::wrap_plots(p1, p2, ncol=2, widths=c(3, 2))
&lt;/code&gt;&lt;/pre&gt;
&lt;/details&gt;
&lt;img src="images/image3-1.png" style="width:80.0%" /&gt;
&lt;p&gt;赤坂は15.1万円、表参道は15.9万円、明治神宮前は16.8万円！高いですね…。他の駅なら同じ金額で二人暮らし用の物件が借りられますね。この辺りはどこも高く、例えば東京メトロ銀座線の外苑前は15.9万円、東急東横線の代官山は15.2万円です。&lt;/p&gt;
&lt;h2 id="おわりに"&gt;おわりに&lt;/h2&gt;
&lt;p&gt;部屋の階数や最寄り駅などによってどの程度家賃相場が変わるのかを定量的に示すことができ、役に立ちそうな結果が得られました。&lt;/p&gt;
&lt;p&gt;ツリー系の機械学習モデルの方が家賃の予測精度は高そうですが、1階は2階と比べて3.6%安いとか、初台～笹塚はほとんど家賃が変わらないといった解釈に使える知見を得るという点では統計モデリングが強いですね。ベイズモデリングなので、築年数効果のような各パラメータや家賃相場の幅をベイズ信用区間という形で知ることができるのもいい点です。&lt;/p&gt;
&lt;p&gt;家賃はまさに階層ベイズ向きのテーマで面白いですね。今後もモデルをブラッシュアップしていきたいです。&lt;/p&gt;
&lt;div class="footnotes" role="doc-endnotes"&gt;
&lt;hr&gt;
&lt;ol&gt;
&lt;li id="fn:1"&gt;
&lt;p&gt;アパートは木造が多くマンションは鉄筋コンクリートが多いですが、木造と鉄筋コンクリートでは耐用年数が異なるため築年数が経過することによる家賃の押し下げ効果が異なると思われます。またアパートは高くても3階程度までですがマンションはより高く建てられることから、部屋の階数による家賃への影響もアパートとマンションで異なりそうです。そのため、この記事では賃貸マンションのみに絞りました。&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:2"&gt;
&lt;p&gt;前の記事では10m2～100m2としていましたが、10m2近辺の物件でモデルの当てはまりが悪いことが分かっています。つまり10m2近辺では面積と家賃の間の関係性が崩れていると思われます。本記事では15m2以上に引き上げました。&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:3"&gt;
&lt;p&gt;地上階の高さが高すぎるマンションや地下階が深すぎるマンションは東京23区の賃貸物件ではわずかなため、階数が家賃に与える効果をロバストに推定する観点からこの条件を加えました。なお、15階以下としているのは、16階以上のマンションはごくわずかであるためです（前回の記事をご参照）。&amp;#160;&lt;a href="#fnref:3" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:4"&gt;
&lt;p&gt;この階層ベイズモデルでは、各最寄り駅における切片と傾きは東京23区全体のそれら（＝23区の平均値）から一定程度ばらついたものであると定式化しています。これは、地価を考慮するとデータ生成のメカニズムに沿っていて理にかなったものです。また、最寄り駅ごとに別々に線形回帰するのではなく東京23区全体の傾向を借用することで、データ数が少ない最寄り駅の物件でもパラメータの推定が行えるのも階層ベイズのメリットです（縮約といいます）。&amp;#160;&lt;a href="#fnref:4" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:5"&gt;
&lt;p&gt;地下1階効果の95%ベイズ信用区間は5.5%-7.7%と若干広いです。これはパラメータ推定に使った物件データの中に地下1階の物件の数があまり多くなかったからです。&amp;#160;&lt;a href="#fnref:5" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:6"&gt;
&lt;p&gt;他に最上階ではないという条件も与えていますが、これまで見たように最上階かどうかは家賃に影響を与えないので、最上階だとしてもプロットは変わりません。&amp;#160;&lt;a href="#fnref:6" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</description></item><item><title>カルマンフィルタで株式のベータ値を推定する</title><link>https://suzunano.net/posts/stock-beta/</link><pubDate>Tue, 16 Jan 2024 00:00:00 +0900</pubDate><guid>https://suzunano.net/posts/stock-beta/</guid><description>&lt;h2 id="概要"&gt;概要&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;個別株式のリスクが市場全体のリスクと比べてどの程度大きいかを示す「ベータ値」という数値があります。&lt;/li&gt;
&lt;li&gt;精緻化された「時変ベータ」を求めるため、ベータ値の変動を状態空間モデルで定式化し、カルマンフィルタを用いて東京電力のベータ値を推定してみました。
&lt;ul&gt;
&lt;li&gt;カルマンフィルタはPythonでフルスクラッチで書きました。なお、参考までにRのKFASを使ったバージョンも付けています。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.amazon.co.jp/dp/425412841X" target="_blank" rel="noopener noreferrer"&gt;経済・ファイナンスのためのカルマンフィルター入門&lt;/a&gt;（森平,
2019）でも述べられているように、東京電力株は株価の値動きが景気変動に影響を受けづらいディフェンシブ銘柄の代表格とされてきましたが、ベータ値は2011年の東日本大震災や原発事故の時期を境に急上昇したことが分かりました。
&lt;ul&gt;
&lt;li&gt;この書籍で用いている状態空間モデルと本記事で用いた状態空間モデルは若干違います（後述）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Pythonでのカルマンフィルタの実装は「カルマンフィルタの実装」の章をご覧ください。結果を見たい方は「ベータ値（カルマンフィルタ版）」の章をご覧ください。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="ベータ値とは"&gt;ベータ値とは&lt;/h2&gt;
&lt;p&gt;TOPIXなどの市場全体の株価と比べて、個別株式などの株価がどの程度大きく動く傾向かというリスクの指標です。個別株式のリスクの大きさを示す指標としてよく使用されます。なお、金融リスクの分野でいうリスクとは、一般的に値動きの激しさを示します。リターンの分布の分散のイメージです。&lt;/p&gt;
&lt;p&gt;例えば、対日経平均株価のベータ値が1.5の銘柄は、日経平均が1%動くと平均的に1.5%動くことを示します。1より大きい銘柄は、値上がるときは市場平均よりも大きく上がるものの、値下がるときは市場平均よりも大きく下がる傾向にあります。一方、1より小さい銘柄は、値上がるときは市場平均よりも上がらないものの、値下がるときは市場平均よりも小幅な下げにとどまりやすいです。&lt;/p&gt;
&lt;p&gt;ベータ値は一般的には0～2程度を取ることが多いです。負の値を取ることもあり得ます。景気に敏感なセクターである電気機器セクターや機械セクターの銘柄は1より大きく、景気に左右されにくい電気・ガスセクターや食品セクターといった内需型のセクターは1より小さい傾向にあります。各銘柄のベータ値は例えば&lt;a href="https://www.nikkei.com/markets/ranking/page/?bd=betahigh" target="_blank" rel="noopener noreferrer"&gt;日経電子版のベータ値高位ランキング&lt;/a&gt;などで見ることができます。&lt;/p&gt;
&lt;p&gt;ベータ値は、個別株式のポジションのヘッジポジションを組むためにも使われます。個別株式のリスクは、市場全体に由来するリスクと個別株式に由来するリスクに分解されます。個別株式の買いポジションを持っているとき、その買いポジションの金額にベータ値を掛けた金額だけ株価指数の売りポジションを持つと、市場全体から来る変動を打ち消すことができます&lt;sup id="fnref:1"&gt;&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref"&gt;1&lt;/a&gt;&lt;/sup&gt;。&lt;/p&gt;
&lt;h2 id="ベータ値の定式化"&gt;ベータ値の定式化&lt;/h2&gt;
&lt;p&gt;$S_{t}, S_{t}^{M}$をそれぞれ、$t (1, \dots, T)$日における個別株式とマーケット指数の終値とします。「マーケット指数」は、日経平均株価やTOPIXなどです。&lt;/p&gt;
&lt;p&gt;$t$日における個別株式とマーケット指数の対前日リターンをそれぞれ$r_{t}, r_{t}^{M}$とすると、対数リターンは以下で計算できます&lt;sup id="fnref:2"&gt;&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref"&gt;2&lt;/a&gt;&lt;/sup&gt;。&lt;/p&gt;
&lt;p&gt;$$
\begin{align*}
r_{t} &amp;amp;= \log S_{t} - \log S_{t-1} \\\
r_{t}^{M} &amp;amp;= \log S_{t}^{M} - \log S_{t-1}^{M}
\end{align*}
$$&lt;/p&gt;
&lt;p&gt;このとき、$t$日におけるベータ値は、以下の$\beta_{t}$です。&lt;/p&gt;
&lt;p&gt;$$
r_{t} = \alpha_{t} + \beta_{t} r_{t}^{M} + \epsilon_t, \quad \epsilon_{t} \sim N(0, \sigma^2)
$$&lt;/p&gt;
&lt;p&gt;$\beta_{t}$は、過去一定期間の$r_{t}, r_{t}^{M}$を用いて回帰で求められます。過去1年～3年＝250営業日～750営業日とすることが多いように思います。この期間を1日ずつずらしてローリング回帰することで各$t$における$\beta_{t}$を得るというのが簡単な推定方法です。&lt;/p&gt;
&lt;p&gt;しかしこの方法は簡略化しているため、いくつか問題点があります。ローリング回帰に用いた標本期間$t-i+1, \dots, t$日の間はベータ値は一定と仮定していますが、実際にはそうではありません。ある日にベータ値が急に動いたとしても、この方法では変動は遅れてマイルドにしか現れません。また$i$をいくつに設定するかによってベータ値が変わります。&lt;/p&gt;
&lt;p&gt;状態空間モデルを用いてベータ値を動的に定式化することで、これらの問題を解消することができます。この記事では、最もオーソドックスだと思われる以下の状態空間モデルを推定します&lt;sup id="fnref:3"&gt;&lt;a href="#fn:3" class="footnote-ref" role="doc-noteref"&gt;3&lt;/a&gt;&lt;/sup&gt;。&lt;/p&gt;
&lt;p&gt;$$
\begin{align*}
r_{t} &amp;amp;= \alpha_{t} + \beta_{t} r_{t}^{M} + e_{t}, &amp;amp;e_t \sim N(0, \sigma_{e}^2) \\\
\alpha_{t} &amp;amp;= \alpha_{t-1} + \epsilon_{t}, &amp;amp;\eta_t \sim N(0, \sigma_{\epsilon}^2) \\\
\beta_{t} &amp;amp;= \beta_{t-1} + \eta_{t}, &amp;amp;\eta_t \sim N(0, \sigma_{\eta}^2)
\end{align*}
$$&lt;/p&gt;
&lt;p&gt;$\beta_{t}$がベータ値です。これは時変ベータと呼ばれます。1本目の式が観測方程式、2本目と3本目の式が状態方程式の状態空間モデルで、ベータ値が日々確率的に変動することを示しています。&lt;/p&gt;
&lt;p&gt;最初のベーシックな方法でのベータ値は、このモデルの2本目と3本目の式をなくしたものですね。時系列回帰が静的から動的になったと考えてもいいです（動的な時系列回帰をするために回帰タイプの状態空間モデルを組むというのはよくあるパターンです）。&lt;/p&gt;
&lt;p&gt;このような、線形で誤差項が正規分布の状態空間モデルでは、状態空間モデルの状態（上のモデルでは$\alpha_{t}, \beta_{t}$）の平均と分散・共分散は、カルマンフィルタというアルゴリズムによって解析的に行列計算で高速に求められます。&lt;/p&gt;
&lt;p&gt;この記事では、まずベータ値とは何かをイメージするために最初の方法でのベータ値を示します。次に後者の時変ベータをカルマンフィルタで実装することで時変ベータをみてみます。&lt;/p&gt;
&lt;h2 id="環境"&gt;環境&lt;/h2&gt;
&lt;p&gt;株価の取得はpandas-datareader, DataFrameのハンドリングはpolars,
プロットはplotnineを使います。&lt;/p&gt;
&lt;p&gt;カルマンフィルタはnumpyとscipyで自分で一から実装しました。最初はpykalmanといういい感じのライブラリを見つけたのですが、メンテが止まっており、&lt;a href="https://github.com/pykalman/pykalman/issues/106" target="_blank" rel="noopener noreferrer"&gt;GitHubのissueに立っているエラー&lt;/a&gt;と同じエラーが出て動きませんでした。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Windows 10&lt;/li&gt;
&lt;li&gt;Python 3.11.5&lt;/li&gt;
&lt;li&gt;numpy 1.26.0&lt;/li&gt;
&lt;li&gt;pandas-datareader 0.10.0&lt;/li&gt;
&lt;li&gt;polars 0.19.6&lt;/li&gt;
&lt;li&gt;plotnine 0.12.3&lt;/li&gt;
&lt;li&gt;patchworklib 0.6.2&lt;/li&gt;
&lt;li&gt;scipy 1.11.3&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class="language-python"&gt;import numpy as np
import scipy as sp
import pandas_datareader.data as pdr
import polars as pl
import patchworklib as pw
from plotnine import *
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id="株価データの取得"&gt;株価データの取得&lt;/h2&gt;
&lt;p&gt;2001/1/5～2023/12/29の東京電力と日経平均株価を取得しました。&lt;/p&gt;
&lt;p&gt;株価は&lt;code&gt;pandas_datareader.data.DataReader&lt;/code&gt;を用いてStooqから取得します。一定期間の個別株式と日経平均の終値が取得できればデータソースは何でも構いません。&lt;/p&gt;
&lt;p&gt;東京電力と日経平均の終値からそれぞれ対前日の対数リターンを計算しておきます。対数リターンは%表記できるように100倍します。また、最初の日のリターンは計算できないので最初の日のレコードを除きます。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-python"&gt;STOCK_CODE = &amp;quot;9501.JP&amp;quot;
MARKET_CODE = &amp;quot;^NKX&amp;quot;
START_DATE = &amp;quot;2001-01-01&amp;quot;
END_DATE = &amp;quot;2023-12-29&amp;quot;
stock = pdr.DataReader(STOCK_CODE, data_source=&amp;quot;stooq&amp;quot;, start=START_DATE, end=END_DATE)
# pd.reset_index()でindexにあるdateをカラムとして持つ
df_stock = pl.from_pandas(stock.reset_index())
df_stock = (
df_stock
.sort(&amp;quot;Date&amp;quot;)
# 数レコードだけ株価がnullの日付があるが、nullの場合は削除する
.filter(pl.col(&amp;quot;Close&amp;quot;).is_not_null())
.with_columns(
Date=pl.col(&amp;quot;Date&amp;quot;).dt.date(),
ret=(pl.col(&amp;quot;Close&amp;quot;).log() - pl.col(&amp;quot;Close&amp;quot;).shift(1).log())*100
)
.slice(1)
)
market = pdr.DataReader(MARKET_CODE, data_source=&amp;quot;stooq&amp;quot;, start=START_DATE, end=END_DATE)
df_market = pl.from_pandas(market.reset_index())
df_market = (
df_market
.sort(&amp;quot;Date&amp;quot;)
.filter(pl.col(&amp;quot;Close&amp;quot;).is_not_null())
.with_columns(
Date=pl.col(&amp;quot;Date&amp;quot;).dt.date(),
ret=(pl.col(&amp;quot;Close&amp;quot;).log() - pl.col(&amp;quot;Close&amp;quot;).shift(1).log())*100
)
.slice(1)
)
df = (
df_stock
.rename({&amp;quot;Date&amp;quot;: &amp;quot;date&amp;quot;, &amp;quot;Close&amp;quot;: &amp;quot;close_stock&amp;quot;, &amp;quot;ret&amp;quot;: &amp;quot;ret_stock&amp;quot;})
.select(&amp;quot;date&amp;quot;, &amp;quot;close_stock&amp;quot;, &amp;quot;ret_stock&amp;quot;)
.join(
df_market
.rename({&amp;quot;Date&amp;quot;: &amp;quot;date&amp;quot;, &amp;quot;Close&amp;quot;: &amp;quot;close_market&amp;quot;, &amp;quot;ret&amp;quot;: &amp;quot;ret_market&amp;quot;})
.select(&amp;quot;date&amp;quot;, &amp;quot;close_market&amp;quot;, &amp;quot;ret_market&amp;quot;),
how=&amp;quot;inner&amp;quot;,
on=&amp;quot;date&amp;quot;
)
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;こんな感じのDataFrameです。closeは終値、retはリターン（%）、_stockは東京電力、_marketは日経平均を指します。&lt;/p&gt;
&lt;p&gt;polarsはDataFrameをprintしたときに各カラムのデータ型を書いてくれるのも素敵ですね。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-python"&gt;print(df)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;shape: (5_632, 5)
┌────────────┬─────────────┬───────────┬──────────────┬────────────┐
│ date ┆ close_stock ┆ ret_stock ┆ close_market ┆ ret_market │
│ --- ┆ --- ┆ --- ┆ --- ┆ --- │
│ date ┆ f64 ┆ f64 ┆ f64 ┆ f64 │
╞════════════╪═════════════╪═══════════╪══════════════╪════════════╡
│ 2001-01-05 ┆ 2800.0 ┆ -2.469261 ┆ 13867.61 ┆ 1.278143 │
│ 2001-01-09 ┆ 2805.0 ┆ 0.178412 ┆ 13610.51 ┆ -1.871362 │
│ 2001-01-10 ┆ 2840.0 ┆ 1.240051 ┆ 13432.65 ┆ -1.315398 │
│ 2001-01-11 ┆ 2850.0 ┆ 0.351494 ┆ 13201.07 ┆ -1.739042 │
│ … ┆ … ┆ … ┆ … ┆ … │
│ 2023-12-26 ┆ 733.1 ┆ 0.684372 ┆ 33305.85 ┆ 0.155709 │
│ 2023-12-27 ┆ 735.0 ┆ 0.258838 ┆ 33681.24 ┆ 1.120795 │
│ 2023-12-28 ┆ 736.7 ┆ 0.231025 ┆ 33539.62 ┆ -0.421358 │
│ 2023-12-29 ┆ 738.5 ┆ 0.244035 ┆ 33464.17 ┆ -0.225211 │
└────────────┴─────────────┴───────────┴──────────────┴────────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id="ベータ値ベーシック版"&gt;ベータ値（ベーシック版）&lt;/h2&gt;
&lt;p&gt;ベーシックな方法でのベータ値は、一定期間の市場全体のリターンを横軸に、個別株式のリターンを縦軸に取って散布図を描き、そこに回帰直線を引いたときの回帰直線の傾きになります。&lt;/p&gt;
&lt;p&gt;以下のプロットは、2023/12/29から直近250営業日のリターンの散布図です。赤い線は回帰直線です。赤い線の傾きが、直近250日のデータから計算する2023/12/29のベータ値になります。1を少し下回るくらいです。&lt;/p&gt;
&lt;details&gt;
&lt;summary&gt;Code&lt;/summary&gt;
&lt;pre&gt;&lt;code class="language-python"&gt;(
ggplot(
df.tail(250),
aes(&amp;quot;ret_market&amp;quot;, &amp;quot;ret_stock&amp;quot;)
)
+geom_point()
+theme_light()
+stat_smooth(method=&amp;quot;lm&amp;quot;, formula=&amp;quot;y ~ x + 1&amp;quot;, se=False, color=&amp;quot;firebrick&amp;quot;)
+labs()
+theme(figure_size=(10, 4), dpi=100)
).draw()
&lt;/code&gt;&lt;/pre&gt;
&lt;/details&gt;
&lt;p&gt;&lt;img src="index_files/figure-commonmark/cell-5-output-1.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;以上の方法で各日ごとにローリング回帰すればベータ値を得ることができます。&lt;/p&gt;
&lt;h2 id="カルマンフィルタのアルゴリズム"&gt;カルマンフィルタのアルゴリズム&lt;/h2&gt;
&lt;p&gt;線形・ガウスの状態空間モデルでは、観測誤差
（今回のモデルでは$\sigma_{e}$）と状態誤差（$\sigma_{\epsilon}, \sigma_{\eta}$）をパラメータとして与えると、各$t (1, \dots, T)$における状態（今回のモデルでは$\alpha_{t}, \beta_{t}$）の平均と共分散行列は行列計算で解析的に得られます。このアルゴリズムをカルマンフィルタといいます。&lt;/p&gt;
&lt;p&gt;ここで得られる状態は以下の2通りです。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;フィルタ化推定量:
各$t$における状態を、$1, \dots, t$の観測値から推定する&lt;/li&gt;
&lt;li&gt;平滑化推定量: 各$t$における状態を、$1, \dots, T$の観測値から推定する&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;後者は全時点のデータから状態をbackwardに推定するため、状態の変化は滑らかに、状態の分散は小さく（信頼区間の幅がフィルタ化推定量より狭く）なります。&lt;/p&gt;
&lt;p&gt;以下にカルマンフィルタのアルゴリズムを示します。&lt;/p&gt;
&lt;p&gt;いま、観測値を$y_{t}$,
状態を$\boldsymbol{x_{t}}$とします。以下、太字の変数は行列、そうではない変数はスカラーを示します。&lt;/p&gt;
&lt;p&gt;行列表現で表すと以下の1本目の式が状態方程式、2本目の式が観測方程式です。&lt;/p&gt;
&lt;p&gt;$$
\begin{align*}
\boldsymbol{x_{t}} &amp;amp;= \boldsymbol{G_{t}} \boldsymbol{x_{t-1}} + \boldsymbol{w_{t}}, &amp;amp;\boldsymbol{w_{t}} \sim N(\boldsymbol{0}, \boldsymbol{W_{t}}) \\\
y_{t} &amp;amp;= \boldsymbol{F_{t}} \boldsymbol{x_{t}} + v_{t}, &amp;amp;v_{t} \sim N(0, V_{t})
\end{align*}
$$&lt;/p&gt;
&lt;p&gt;ただし、$\boldsymbol{G_{t}}$は状態遷移行列（p x
p）、$\boldsymbol{F_{t}}$は観測行列（1 x
p）、$\boldsymbol{W_{t}}$は状態誤差の共分散行列（p x
p）、$V_{t}$は観測誤差の分散です。&lt;/p&gt;
&lt;p&gt;また、状態の事前分布$\boldsymbol{x_{0}}$は$\boldsymbol{x_{0}}\sim N(\boldsymbol{m_{0}}, \boldsymbol{C_{0}})$であり、$\boldsymbol{m_{0}}$はp次元のベクトル、$\boldsymbol{C_{0}}$はp
x pの行列です。&lt;/p&gt;
&lt;h3 id="フィルタリング"&gt;フィルタリング&lt;/h3&gt;
&lt;p&gt;$t-1$での状態のフィルタリング分布の平均と共分散行列$\boldsymbol{m_{t-1}}, \boldsymbol{C_{t-1}}$（それぞれp次元ベクトル、p
x pの行列）が与えられると、&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;フィルタリング分布: $N(\boldsymbol{m_{t}}, \boldsymbol{C_{t}})$
&lt;ul&gt;
&lt;li&gt;$\boldsymbol{m_{t}}, \boldsymbol{C_{t}}$はそれぞれp次元ベクトル、p x
pの行列&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;一期先予測分布: $N(\boldsymbol{a_{t-1}}, \boldsymbol{R_{t-1}})$
&lt;ul&gt;
&lt;li&gt;$\boldsymbol{a_{t}}, \boldsymbol{R_{t}}$はそれぞれp次元ベクトル、p x
pの行列&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;一期先予測尤度: $N(f_{t}, Q_{t})$
&lt;ul&gt;
&lt;li&gt;$f_{t}, Q_{t}$はスカラー&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;は以下で計算できます。&lt;/p&gt;
&lt;p&gt;$$
\begin{align*}
\boldsymbol{a_{t}} &amp;amp;= \boldsymbol{G_{t}} \boldsymbol{m_{t-1}} \\\
\boldsymbol{R_{t}} &amp;amp;= \boldsymbol{G_{t}} \boldsymbol{C_{t-1}} \boldsymbol{G_{t}}^{\mathrm{T}} + \boldsymbol{W_{t}} \\\
f_{t} &amp;amp;= \boldsymbol{F_{t}} \boldsymbol{a_{t}} \\\
Q_{t} &amp;amp;= \boldsymbol{F_{t}} \boldsymbol{R_{t}} \boldsymbol{F_{t}}^{\mathrm{T}} + V_{t} \\\
\boldsymbol{K_{t}} &amp;amp;= \boldsymbol{R_{t}} \boldsymbol{F_{t}}^{\mathrm{T}} Q_{t}^{-1} \\\
\boldsymbol{m_{t}} &amp;amp;= \boldsymbol{a_{t}} + \boldsymbol{K_{t}} (y_{t} - f_{t}) \\\
\boldsymbol{C_{t}} &amp;amp;= (\boldsymbol{I} - \boldsymbol{K_{t}} \boldsymbol{F_{t}}) \boldsymbol{R_{t}}
\end{align*}
$$&lt;/p&gt;
&lt;p&gt;$\boldsymbol{K_{t}}$はカルマンゲインと呼ばれるものです。$\boldsymbol{I}$は単位行列です。&lt;/p&gt;
&lt;h3 id="平滑化"&gt;平滑化&lt;/h3&gt;
&lt;p&gt;$t+1$での状態の平滑化分布の平均と共分散行列$\boldsymbol{s_{t+1}}, \boldsymbol{S_{t+1}}$（それぞれp次元ベクトル、p
x pの行列）が与えられると、平滑化分布
$N(\boldsymbol{s_{t}}, \boldsymbol{S_{t}})$は以下で計算できます。&lt;/p&gt;
&lt;p&gt;$$
\begin{align*}
\boldsymbol{A_{t}} &amp;amp;= \boldsymbol{C_{t}} \boldsymbol{G_{t+1}}^{\boldsymbol{T}} \boldsymbol{R_{t+1}}^{-1} \\\
\boldsymbol{s_{t}} &amp;amp;= \boldsymbol{m_{t}} + \boldsymbol{A_{t}} (\boldsymbol{s_{t+1}} - \boldsymbol{a_{t+1}}) \\\
\boldsymbol{S_{t}} &amp;amp;= \boldsymbol{C_{t}} + \boldsymbol{A_{t}} (\boldsymbol{S_{t+1}} - \boldsymbol{R_{t+1}}) \boldsymbol{A_{t}}^{\boldsymbol{T}}
\end{align*}
$$&lt;/p&gt;
&lt;p&gt;$\boldsymbol{A_{t}}$は平滑化利得と呼ばれるものです。&lt;/p&gt;
&lt;h3 id="対数尤度"&gt;対数尤度&lt;/h3&gt;
&lt;p&gt;観測誤差と状態誤差をパラメータとして与えると、一期先予測尤度から尤度を解析的に計算できます。対数尤度は以下で求められます。&lt;/p&gt;
&lt;p&gt;$$
\begin{align*}
loglik(\boldsymbol{G_{t}}, \boldsymbol{F_{t}}, \boldsymbol{W_{t}}, V_{t}, \boldsymbol{m_{0}}, \boldsymbol{C_{0}}) &amp;amp;= \sum_{t=1}^{T} \log p(y_{t} | y_{1:t-1}; \boldsymbol{G_{t}}, \boldsymbol{F_{t}}, \boldsymbol{W_{t}}, V_{t}, \boldsymbol{m_{0}}, \boldsymbol{C_{0}}) \\\
&amp;amp;= -\frac{1}{2} T \log 2 \pi - \frac{1}{2} \sum_{t=1}^{T} log |Q_{t}| - \frac{1}{2} \sum_{t=1}^{T} (y_{t} - f_{t})^2 / Q_{t}
\end{align*}
$$&lt;/p&gt;
&lt;p&gt;対数尤度を最大化するような観測誤差と状態誤差の値を数理最適化で求めるプロセスを最初に行い、この観測誤差と状態誤差をパラメータとして用いてフィルタリングと平滑化を行います。&lt;/p&gt;
&lt;p&gt;以上のアルゴリズムの導出はこちらの書籍をご参照ください。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.amazon.co.jp/dp/4774196460" target="_blank" rel="noopener noreferrer"&gt;基礎からわかる時系列分析
―Rで実践するカルマンフィルタ・MCMC・粒子フィルター&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;日本語で読める状態空間モデルの本では最高レベルに充実していると思います。&lt;/li&gt;
&lt;li&gt;Rのコードでの実装例もあるので大変参考になります。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="カルマンフィルタの実装"&gt;カルマンフィルタの実装&lt;/h2&gt;
&lt;p&gt;いま推定したいベータ値のモデルは、上の行列形式の状態空間モデルでそれぞれ以下としたものです。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$y_{t}$: $r_{t}$&lt;/li&gt;
&lt;li&gt;$\boldsymbol{F_{t}}$: $(1, r_{t}^{M})$&lt;/li&gt;
&lt;li&gt;$\boldsymbol{x_{t}}$: $(\alpha_{t}, {\beta_{t}})^{\mathrm{T}}$&lt;/li&gt;
&lt;li&gt;$\boldsymbol{G_{t}}$: 2 x 2の単位行列&lt;/li&gt;
&lt;li&gt;$\boldsymbol{w_{t}}$: 左上が$W_{\alpha}$, 右下が$W_{\beta}$の2 x
2の対角行列&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;以下のコードがカルマンフィルタの実装です。上で示したアルゴリズムをそのまま実装します。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-python"&gt;def filtering(y, m, C, G, F, W, V):
&amp;quot;&amp;quot;&amp;quot;
(t-1)期において、1期先（t期）のフィルタリングを行う関数
such as:
x_t = G_t * x_(t-1) + w_t, w_t ~ N(0, W_t) : 状態方程式
y_t = F_t * x_t + v_t, v_t ~ N(0, V_t) : 観測方程式
x, G, w, W, Fは行列, y, v, Vはスカラー
Params:
y: 観測値 [時点t]
m, C: 状態の平均, 共分散行列 [t-1]
G, F, W, V: 状態遷移行列, 観測行列, 状態誤差の共分散行列, 観測誤差の共分散行列 [t]
Returns:
tuple
フィルタリング分布の平均と共分散行列 m, C [t]
一期先予測分布の平均と共分散行列 a, R [t]
一期先予測尤度の平均と共分散行列 f, Q [t]
&amp;quot;&amp;quot;&amp;quot;
# 一期先予測分布
a = G @ m
R = G @ C @ G.T + W
# 一期先予測尤度
f = F @ a
Q = F @ R @ F.T + V
# カルマンゲイン
K = R @ F.T @ np.linalg.inv(Q)
# 状態の更新
m = a + K @ (y - f)
C = R - K @ F @ R
f_scalar, Q_scalar = f.item(), Q.item()
return m, C, a, R, f_scalar, Q_scalar
def smoothing(s, S, m, C, a, R, G):
&amp;quot;&amp;quot;&amp;quot;
(t+1)期のsとSからt期のsとSを求める（状態の平滑化分布の平均と共分散行列）
Params:
s, S: 平滑化分布の平均, 共分散行列 [t+1]
m, C: 状態の平均, 共分散行列 [t]
a, R: 一期先予測分布の平均と共分散行列 [t+1]
G: 状態遷移行列 [t+1]
Returns:
tuple
平滑化分布の平均, 共分散行列 s, S [t]
&amp;quot;&amp;quot;&amp;quot;
# 平滑化利得
A = C @ G.T @ np.linalg.inv(R)
# 平滑化された状態
s = m + A @ (s - a)
S = C + A @ (S - R) @ A.T
return s, S
def reverse_loglik(w_v, dims, y, G, F, m0, C0):
&amp;quot;&amp;quot;&amp;quot;
状態誤差と観測誤差を与えると対数尤度の-1倍を返す関数
Params:
w_v: 長さ2のtuple　状態誤差と観測誤差の値
dims: 状態の数
y, G, F, m0, C0: 状態空間モデルの係数
Returns:
float: 対数尤度の-1倍
&amp;quot;&amp;quot;&amp;quot;
# 分散は負にならないのでexpをかける
W = np.eye(dims) * np.exp(w_v[0])
V = np.array([1]).reshape((1, 1)) * np.exp(w_v[1])
T = len(y)
m, C = np.zeros((T, dims)), np.zeros((T, dims, dims))
a, R = np.zeros((T, dims)), np.zeros((T, dims, dims))
f, Q = np.zeros((T)), np.zeros((T))
# 全期間フィルタリングする
for t in range(0, T):
_F = F[t].reshape((1, dims))
if t == 0:
m[t], C[t], a[t], R[t], f[t], Q[t] = filtering(y[t], m0, C0, G, _F, W, V)
else:
m[t], C[t], a[t], R[t], f[t], Q[t] = filtering(y[t], m[t-1], C[t-1], G, _F, W, V)
loglik = (-1) * np.sum(np.log(Q)) / 2 - (np.sum((y - f)**2 / Q)) / 2
return (-1)*loglik
# 対数尤度を最大化する観測誤差と状態誤差を求める
ret_market = df.get_column(&amp;quot;ret_market&amp;quot;).to_numpy()
ret_stock = df.get_column(&amp;quot;ret_stock&amp;quot;).to_numpy()
y = ret_stock
x = ret_market
T = len(y)
dims = 2
G = np.eye(dims)
F = np.eye(T, dims)
F[:, 0] = 1
F[:, 1] = x
# 状態の平均と共分散行列の初期値
m0 = np.zeros(dims)
C0 = np.eye(dims)*10000000
best_par=sp.optimize.minimize(
reverse_loglik,
[0.0, 0.0],
args=(dims, y, G, F, m0, C0),
method=&amp;quot;BFGS&amp;quot;
)
W = np.eye(dims) * np.exp(best_par.x[0])
V = np.array([1]).reshape((1, 1)) * np.exp(best_par.x[1])
# 上で求めた観測誤差と状態誤差をもとにフィルタリングと平滑化を行う
# 結果を入れる変数
m, C = np.zeros((T, dims)), np.zeros((T, dims, dims))
a, R = np.zeros((T, dims)), np.zeros((T, dims, dims))
f, Q = np.zeros((T)), np.zeros((T))
s, S = np.zeros((T, dims)), np.zeros((T, dims, dims))
# フィルタリング
for t in range(0, T):
_F = F[t].reshape((1, dims))
if t == 0:
m[t], C[t], a[t], R[t], f[t], Q[t] = filtering(y[t], m0, C0, G, _F, W, V)
else:
m[t], C[t], a[t], R[t], f[t], Q[t] = filtering(y[t], m[t-1], C[t-1], G, _F, W, V)
# 平滑化
for t in range(T - 1, 0, -1):
if t == T - 1:
s[t], S[t] = m[t], C[t]
else:
s[t], S[t] = smoothing(s[t+1], S[t+1], m[t], C[t], a[t+1], R[t+1], G)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;20秒くらいで推定できます。&lt;/p&gt;
&lt;p&gt;scipyの&lt;code&gt;scipy.optimize.minimize&lt;/code&gt;は最小化なので、対数尤度の-1倍を最小化の目的関数とします。&lt;code&gt;scipy.optimize.minimize&lt;/code&gt;に渡す初期値ですが、与える初期値によっては局所解に落ちるので、初期値を複数与えて目的関数が最小となる初期値をグリッドサーチで求めるのが望ましいです。&lt;/p&gt;
&lt;p&gt;RやPythonなどのよくできたライブラリだとよくあるタイプのモデルは行列形式で書かなくても記述できますが、少しカスタマイズしようとすると、ライブラリを使っても行列表現して自分で係数行列を与えてあげることが必要になります。&lt;/p&gt;
&lt;p&gt;スクラッチで実装するにしてもライブラリを使うにしても、カルマンフィルタの実装のポイントは、推定したいモデルを行列形式で書くこと、各パラメータの行列の次元（m
x
n）と何が行列で何がスカラーなのかを意識することだと思います。意識しないと混乱するんですよね。&lt;/p&gt;
&lt;h2 id="ベータ値カルマンフィルタ版"&gt;ベータ値（カルマンフィルタ版）&lt;/h2&gt;
&lt;p&gt;フィルタ化と平滑化の状態の平均と共分散行列を取り出して95%信頼区間を計算します。扱いやすいようにnumpy.ndarrayからpolars.DataFrameに変換して持っておきます。&lt;/p&gt;
&lt;details&gt;
&lt;summary&gt;Code&lt;/summary&gt;
&lt;pre&gt;&lt;code class="language-python"&gt;# フィルタ化と平滑化の平均と共分散行列を取り出し、そこから95%信頼区間を計算する
beta_est = (
pl.DataFrame({
&amp;quot;date&amp;quot;: df.get_column(&amp;quot;date&amp;quot;),
&amp;quot;estimated&amp;quot;: m[:, 1],
&amp;quot;std_error&amp;quot;: np.sqrt(C[:, 1, 1])
})
.with_columns(
lower=pl.col(&amp;quot;estimated&amp;quot;)+sp.stats.norm.ppf(0.025)*pl.col(&amp;quot;std_error&amp;quot;),
upper=pl.col(&amp;quot;estimated&amp;quot;)+sp.stats.norm.ppf(0.975)*pl.col(&amp;quot;std_error&amp;quot;),
)
)
alpha_est = (
pl.DataFrame({
&amp;quot;date&amp;quot;: df.get_column(&amp;quot;date&amp;quot;),
&amp;quot;estimated&amp;quot;: m[:, 0],
&amp;quot;std_error&amp;quot;: np.sqrt(C[:, 0, 0])
})
.with_columns(
lower=pl.col(&amp;quot;estimated&amp;quot;)+sp.stats.norm.ppf(0.025)*pl.col(&amp;quot;std_error&amp;quot;),
upper=pl.col(&amp;quot;estimated&amp;quot;)+sp.stats.norm.ppf(0.975)*pl.col(&amp;quot;std_error&amp;quot;),
)
)
beta_smooth = (
pl.DataFrame({
&amp;quot;date&amp;quot;: df.get_column(&amp;quot;date&amp;quot;),
&amp;quot;estimated&amp;quot;: s[:, 1],
&amp;quot;std_error&amp;quot;: np.sqrt(S[:, 1, 1])
})
.with_columns(
lower=pl.col(&amp;quot;estimated&amp;quot;)+sp.stats.norm.ppf(0.025)*pl.col(&amp;quot;std_error&amp;quot;),
upper=pl.col(&amp;quot;estimated&amp;quot;)+sp.stats.norm.ppf(0.975)*pl.col(&amp;quot;std_error&amp;quot;),
)
)
alpha_smooth = (
pl.DataFrame({
&amp;quot;date&amp;quot;: df.get_column(&amp;quot;date&amp;quot;),
&amp;quot;estimated&amp;quot;: s[:, 0],
&amp;quot;std_error&amp;quot;: np.sqrt(S[:, 0, 0])
})
.with_columns(
lower=pl.col(&amp;quot;estimated&amp;quot;)+sp.stats.norm.ppf(0.025)*pl.col(&amp;quot;std_error&amp;quot;),
upper=pl.col(&amp;quot;estimated&amp;quot;)+sp.stats.norm.ppf(0.975)*pl.col(&amp;quot;std_error&amp;quot;),
)
)
&lt;/code&gt;&lt;/pre&gt;
&lt;/details&gt;
&lt;p&gt;次のプロットの1枚目はベータ値（$\beta_{t}$）のフィルタ化推定量、2枚目は平滑化推定量、3枚目は東京電力の終値です。赤い線は平均値、上下の青いリボンは95%信頼区間です。ただし最初の50営業日は推定が安定していないので51営業日以降をプロットしています。&lt;/p&gt;
&lt;details&gt;
&lt;summary&gt;Code&lt;/summary&gt;
&lt;pre&gt;&lt;code class="language-python"&gt;# 結果のプロット
# 最初の50期は使わない
p1 = (
ggplot(beta_est.slice(50), aes(&amp;quot;date&amp;quot;))+
theme_light()+
geom_ribbon(aes(ymin=&amp;quot;lower&amp;quot;, ymax=&amp;quot;upper&amp;quot;), fill=&amp;quot;lightsteelblue&amp;quot;, alpha=0.5)+
geom_line(aes(y=&amp;quot;lower&amp;quot;), color=&amp;quot;lightsteelblue&amp;quot;)+
geom_line(aes(y=&amp;quot;upper&amp;quot;), color=&amp;quot;lightsteelblue&amp;quot;)+
geom_line(aes(y=&amp;quot;estimated&amp;quot;), color=&amp;quot;firebrick&amp;quot;)+
scale_x_date(breaks=&amp;quot;1 year&amp;quot;, date_labels=&amp;quot;%y&amp;quot;)+
scale_y_continuous(breaks=range(-1, 3, 1))+
labs(
title=&amp;quot;[9501: Tepco HD] time-varing beta (filtered); red: estimated (mean), light blue: 95%CI&amp;quot;,
x=&amp;quot;date (year)&amp;quot;,
y=&amp;quot;beta&amp;quot;
)
)
p2 = (
ggplot(beta_smooth.slice(50), aes(&amp;quot;date&amp;quot;))+
theme_light()+
geom_ribbon(aes(ymin=&amp;quot;lower&amp;quot;, ymax=&amp;quot;upper&amp;quot;), fill=&amp;quot;lightsteelblue&amp;quot;, alpha=0.5)+
geom_line(aes(y=&amp;quot;lower&amp;quot;), color=&amp;quot;lightsteelblue&amp;quot;)+
geom_line(aes(y=&amp;quot;upper&amp;quot;), color=&amp;quot;lightsteelblue&amp;quot;)+
geom_line(aes(y=&amp;quot;estimated&amp;quot;), color=&amp;quot;firebrick&amp;quot;)+
scale_x_date(breaks=&amp;quot;1 year&amp;quot;, date_labels=&amp;quot;%y&amp;quot;)+
scale_y_continuous(breaks=range(-1, 3, 1))+
labs(
title=&amp;quot;[9501: Tepco HD] time-varing beta (smoothed); red: estimated (mean), light blue: 95%CI&amp;quot;,
x=&amp;quot;date (year)&amp;quot;,
y=&amp;quot;beta&amp;quot;
)
)
p3 = (
ggplot(df.slice(50), aes(&amp;quot;date&amp;quot;, &amp;quot;close_stock&amp;quot;))+
theme_light()+
geom_line()+
scale_x_date(breaks=&amp;quot;1 year&amp;quot;, date_labels=&amp;quot;%y&amp;quot;)+
labs(
title=&amp;quot;[9501: Tepco HD] stock price (close)&amp;quot;,
x=&amp;quot;date (year)&amp;quot;,
y=&amp;quot;close&amp;quot;
)
)
pw.load_ggplot(p1, figsize=(10, 2)) / pw.load_ggplot(p2, figsize=(10, 2)) / pw.load_ggplot(p3, figsize=(10, 2))
&lt;/code&gt;&lt;/pre&gt;
&lt;/details&gt;
&lt;p&gt;&lt;img src="index_files/figure-commonmark/cell-8-output-1.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;状態空間モデルを用いたことで、ベータ値の推定値だけでなく、ベータ値の95%信頼区間を求めることができます。ベータ値は95%の確率でこの範囲内という幅を得られるのもうれしいですね。&lt;/p&gt;
&lt;p&gt;一番上のプロットを見ると分かりますが、2011年3月の東日本大震災までは、ベータ値の平均は1を下回っています。東京電力株は景気変動の影響を受けにくいディフェンシブ銘柄の代表ともいえる銘柄でしたが、東日本大震災のころにベータ値が急変動し、一時は2程度まで上がっていることが分かります。&lt;/p&gt;
&lt;p&gt;2枚目のプロットは平滑化推定量なので、1枚目のプロットを滑らかにしたような感じになっていて大まかなトレンドが分かりやすいですね。ただし、全期間の観測値から状態を推定するという平滑化のアルゴリズム上、ベータ値は東日本大震災の前から大きく上昇しています。&lt;/p&gt;
&lt;p&gt;東日本大震災のころにベータ値が急変動している背景は、原発事故やそれによる経営環境の変化だと想像はできますが、状態空間モデルではそうであるという因果関係は何も示していないことに注意が必要です。しかし、冒頭で触れた「経済・ファイナンスのためのカルマンフィルター入門」では、原発を持たない沖縄電力以外の電力会社は東京電力と同様に震災を境にベータの振る舞いが変わっていて、沖縄電力だけは特に変化がなかったと述べられています。&lt;/p&gt;
&lt;h2 id="おわりに"&gt;おわりに&lt;/h2&gt;
&lt;p&gt;状態空間モデルとカルマンフィルタによってベータ値の変動をとらえることができました。&lt;/p&gt;
&lt;p&gt;状態誤差と観測誤差は正規分布としましたが、ベータ値の状態空間モデルでは特に状態誤差は正規分布ではないという先行研究&lt;sup id="fnref:4"&gt;&lt;a href="#fn:4" class="footnote-ref" role="doc-noteref"&gt;4&lt;/a&gt;&lt;/sup&gt;もあるので、状態誤差をt分布などにしたモデルを組んでみても面白そうです。&lt;/p&gt;
&lt;h2 id="appendix-1-r--kfasでのカルマンフィルタの実装"&gt;[Appendix. 1] R + KFASでのカルマンフィルタの実装&lt;/h2&gt;
&lt;p&gt;Rで実装する場合の例です。Rでカルマンフィルタをやるなら、KFASという、dlmに並んでデファクトスタンダードのライブラリがあります。&lt;/p&gt;
&lt;p&gt;Pythonの場合と同じように、&lt;code&gt;df&lt;/code&gt;というdata.frameに、ret_stockとret_marketというカラムを持っている前提です。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-r"&gt;library(KFAS)
# 状態空間モデルの定義
mod &amp;lt;- KFAS::SSModel(
# 観測誤差の分散
H=NA,
# SSMregression内の-1は状態方程式に切片がないことを示す
# Qは状態誤差の分散
ret_stock ~ KFAS::SSMregression(~ret_market-1, Q=NA),
data=df
)
# 対数尤度を最大化する観測誤差と状態誤差を数理最適化で求める
fit &amp;lt;- KFAS::fitSSM(mod, inits=c(0,0), method=&amp;quot;BFGS&amp;quot;)
# フィルタリングと平滑化をする
kfs &amp;lt;- KFAS::KFS(fit$model, filtering=c(&amp;quot;state&amp;quot;, &amp;quot;mean&amp;quot;), smoothing=c(&amp;quot;state&amp;quot;, &amp;quot;mean&amp;quot;))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;これだけです。楽ですね。今回の回帰タイプの線形・ガウスの状態空間モデルは&lt;code&gt;KFAS::SSMregression()&lt;/code&gt;という関数でサポートされていますのでこれを呼ぶだけです。&lt;/p&gt;
&lt;p&gt;フィルタ化推定量の平均と共分散行列、平滑化推定量の平均と共分散行列をそれぞれ&lt;code&gt;kfs$att, kfs$Ptt, kfs$alphahat, kfs$V&lt;/code&gt;で取り出して、Python版と同じようなベータ値の時系列プロットを描くことができます。&lt;/p&gt;
&lt;h2 id="appendix-2-ベータ値の定式化リスクフリーレートバージョン"&gt;[Appendix. 2] ベータ値の定式化（リスクフリーレートバージョン）&lt;/h2&gt;
&lt;p&gt;ベータ値の推定モデルは、リスクフリーレート&lt;sup id="fnref:5"&gt;&lt;a href="#fn:5" class="footnote-ref" role="doc-noteref"&gt;5&lt;/a&gt;&lt;/sup&gt;を取り入れたバージョンもあります。&lt;/p&gt;
&lt;p&gt;いま、$t$日におけるリスクフリーレートを$r_{t}^{f}$としたとき、最初の簡略化したモデルは以下になります。&lt;/p&gt;
&lt;p&gt;$$
r_{t} - r_{t}^{f} = \beta_{t} (r_{t}^{M} - r_{t}^{f}) + \epsilon_t, \quad \epsilon_{t} \sim N(0, \sigma^2)
$$&lt;/p&gt;
&lt;p&gt;また、状態空間モデルバージョンはこちらになります。&lt;/p&gt;
&lt;p&gt;$$
\begin{aligned}
r_{t} - r_{t}^{f} &amp;amp;= \beta_{t} (r_{t}^{M} - r_{t}^{f}) + e_{t}, \quad e_t \sim N(0, \sigma_{e}^2) \\\
\beta_{t} &amp;amp;= \beta_{t-1} + \eta_{t}, \quad \eta_t \sim N(0, \sigma_{\eta}^2)
\end{aligned}
$$&lt;/p&gt;
&lt;p&gt;どちらも、$\alpha_{t}$の項がなくなっています。&lt;/p&gt;
&lt;p&gt;$r_{t} - r_{t}^{f}$と$r_{t}^{M} - r_{t}^{f}$はそれぞれ、リスクを負って得られる個別株式とマーケット指数の超過リターンです。そのため、$\alpha_{t}$が正であれば、リスクを全く負わずに、リターンがリスクフリーレート+$\alpha_{t}$だけ得られることを示します。そのような裁定取引の機会は効率的な市場では得られないというファイナンスの無裁定理論より、$\alpha_{t}$は0となります&lt;sup id="fnref:6"&gt;&lt;a href="#fn:6" class="footnote-ref" role="doc-noteref"&gt;6&lt;/a&gt;&lt;/sup&gt;。なお、これは理論だけの話ではなく、$\alpha_{t}$をモデルに入れて推定すると、多くの場合で$\alpha_{t}$は有意に正でも負でもないことが実証的にも示されます。&lt;/p&gt;
&lt;p&gt;ただし、日本のリスクフリーレートはほぼ0のため、リスクフリーレートを考慮してもしなくても大きくは変わりません。&lt;/p&gt;
&lt;h2 id="関連記事"&gt;関連記事&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="../stochastic-volatility-model/"&gt;TOPIXのボラティリティをStochastic Volatilityモデル + R +
Stanで推定する - suzuna's memo&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="footnotes" role="doc-endnotes"&gt;
&lt;hr&gt;
&lt;ol&gt;
&lt;li id="fn:1"&gt;
&lt;p&gt;このヘッジによって市場平均のリターンとの差分だけを得ることができます。例えば市場平均が-5%、個別株式のポジションが-3%となった場合、-3%-(-5%)=2%のリターンを得られます。個別株式のポジションが値下がった場合でも市場平均とのリターンの差分だけを得られるというのがメリットです。詳しく知りたい方はこちらの本の第3章をご参照ください:
ジョン・ハル (2016),
「ファイナンシャルエンジニアリング（第9版）」。ただし実際には、個別株式や株価指数（先物）の最低投資単位の都合上ベータ値を厳密に0にするポジションは組みづらいこと、ヘッジ比率を調整することの手数料の考慮、連続的な値動きに対してヘッジ比率の調整は日次や週次、月次など離散的なタイミングでしか行えず厳密にヘッジができないことなどの難しさがあります。&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:2"&gt;
&lt;p&gt;リターンと聞いてふつうイメージするのは、$r_{t} = (S_{t} / S_{t-1}) - 1$だと思います。金融工学では当日の株価の自然対数と前日の株価の自然対数の差である対数リターンを用います。株価がたいてい取りうる数%くらいのリターンの範囲では、テイラー展開より対数リターンは割り算でのリターンとほぼ一致します。&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:3"&gt;
&lt;p&gt;なお、「経済・ファイナンスのためのカルマンフィルター入門」（森平,
2019）では、$\alpha$は時変ではないモデルを推定しています。&amp;#160;&lt;a href="#fnref:3" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:4"&gt;
&lt;p&gt;矢野浩一 (2004),
「カルマンフィルタによるベータ推定」2004年度FSAリサーチレビュー,
104-125.&amp;#160;&lt;a href="#fnref:4" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:5"&gt;
&lt;p&gt;ここでいうリスクフリーレートには、現在では無担保コール翌日物金利やOISレートが用いられます。リスクフリーレートとみなされることの多い国債の利回りは、日本国の信用リスクが含まれるため厳密にはリスクフリーなレートではありません。無担保コール翌日物金利やOISは信用リスクをほとんど含まない金利のため、リスクフリーレートとして望ましいです。リスクフリーレートにどの金利を使うかというのはリーマンショック以降特に意識されるようになったと聞きます。リスクフリーレートについて知りたい方はこの辺が参考になるかと思います。&lt;a href="https://www.mof.go.jp/public_relations/finance/202112/202112e.html" target="_blank" rel="noopener noreferrer"&gt;リスク・フリー・レート（RFR）入門－TONA，TORF，OISを中心に－&lt;/a&gt;/&lt;a href="https://www.mof.go.jp/pri/research/special_report/f202008_01..pdf" target="_blank" rel="noopener noreferrer"&gt;金利スワップ入門
-基礎編-&lt;/a&gt;&amp;#160;&lt;a href="#fnref:5" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:6"&gt;
&lt;p&gt;逆に言うと、競争が働いている効率的な市場ではない市場では、そうではない可能性はあります。&amp;#160;&lt;a href="#fnref:6" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</description></item><item><title>tcardgenとGitHub ActionsでHugoのブログのOGPを動的に作る</title><link>https://suzunano.net/posts/hugo-ogp/</link><pubDate>Wed, 27 Dec 2023 00:00:00 +0900</pubDate><guid>https://suzunano.net/posts/hugo-ogp/</guid><description>&lt;h2 id="概要"&gt;概要&lt;/h2&gt;
&lt;p&gt;markdownファイルからOGP画像を生成する&lt;a href="https://github.com/Ladicle/tcardgen" target="_blank" rel="noopener noreferrer"&gt;Ladicle/tcardgen&lt;/a&gt;を使い、Hugoで作っているブログで記事のmarkdownファイルをGitHubにpushすると、記事のOGP画像を動的に作るGitHub Actionsを作りました。動的というのは、markdownファイルのFront Matterからtitleなどを取り出してOGP画像を作るということです。&lt;/p&gt;
&lt;p&gt;こんな感じです。&lt;/p&gt;
&lt;img src="ogp-screenshot.png" width=500&gt;
&lt;p&gt;OGP画像には&lt;a href="http://azukifont.com/index.html" target="_blank" rel="noopener noreferrer"&gt;うずらフォント&lt;/a&gt;を使いました。わたしの好きなかわいいフォントを選べてうれしいです。こういうのがモチベーションを上げるのです。&lt;/p&gt;
&lt;p&gt;GitHub Actionsではなくローカルでtcardgenを実行してもいいですが、HugoのビルドもGitHub Actionsで行っている場合、markdownファイルを書いてpushするだけでビルドもOGP画像生成も自動でできちゃうのでとっても快適です。&lt;/p&gt;
&lt;p&gt;このブログはHugo（テーマは&lt;a href="https://github.com/Mitrichius/hugo-theme-anubis" target="_blank" rel="noopener noreferrer"&gt;anubis&lt;/a&gt;）のページバンドルとして、&lt;a href="https://gohugo.io/content-management/page-bundles/" target="_blank" rel="noopener noreferrer"&gt;Leaf Bundle&lt;/a&gt;を採用しています。tcardgenをLeaf Bundleで動かすところとHugoのテーマにOGPを入れるのにちょっと苦戦したので、HugoのLeaf Bundleを採用しているサイトでtcardgenによってOGP画像をGitHub Actionsで作る方法をご紹介します。&lt;/p&gt;
&lt;p&gt;大手ブログや技術ブログサイトなら何もしなくてもOGPを作ってくれるのでこういう手間はありませんが、自分だけのお城をHugoで好きなように建てていくのも悪くないです。使い慣れたエディタ（VSCode）とGitとGitHubで全てが完結できるのもいいですね。&lt;/p&gt;
&lt;h2 id="技術構成"&gt;技術構成&lt;/h2&gt;
&lt;p&gt;技術構成はこのようになっています。&lt;/p&gt;
&lt;p&gt;もともと、Hugoのソースのリポジトリ（suzuna/blog-source）にpushすると、GitHub ActionsによってHugoのビルドを行い、生成物をHugoの公開用のリポジトリ（suzuna/blog）にpushするようにしていたのですが、このGitHub ActionsにtcardgenでのOGP画像生成を加えました。&lt;/p&gt;
&lt;img src="architecture.png"&gt;
&lt;h2 id="hugoのディレクトリ構成"&gt;Hugoのディレクトリ構成&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;content/&lt;/code&gt;以下はこのような構成になっています。&lt;code&gt;content/posts&lt;/code&gt;以下に、記事を書いた日付を&lt;code&gt;yyyymmdd&lt;/code&gt;で先頭に付与したフォルダを作り、その中に記事を&lt;code&gt;index.md&lt;/code&gt;で作成します。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-text"&gt;content
├── about.md
└── posts
├── yyyymmdd-hoge-fuga-piyo
│   └── index.md
└── yyyymmdd-foo-bar-baz
├── images
│   └── image01.jpg
└── index.md
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id="やったこと"&gt;やったこと&lt;/h2&gt;
&lt;h3 id="tcardgenの出力ogp画像のファイル名の調整"&gt;tcardgenの出力OGP画像のファイル名の調整&lt;/h3&gt;
&lt;p&gt;tcardgenで、特定のディレクトリ以下にある全てのmarkdownファイルについてそれぞれOGP画像を作るなら、まずこちらを行います。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Goをインストールする&lt;/li&gt;
&lt;li&gt;&lt;code&gt;path/to/fontDir&lt;/code&gt;以下に、&lt;code&gt;&amp;lt;font-name&amp;gt;-Bold.ttf&lt;/code&gt;, &lt;code&gt;&amp;lt;font-name&amp;gt;-Medium.ttf&lt;/code&gt;, &lt;code&gt;&amp;lt;font-name&amp;gt;-Regular.ttf&lt;/code&gt;のファイル名で3個のフォントを置く&lt;sup id="fnref:1"&gt;&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref"&gt;1&lt;/a&gt;&lt;/sup&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;path/to/templateFile&lt;/code&gt;というファイル名でOGP画像のテンプレートファイルを置く&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;そして、tcardgenのREADMEのとおり以下を実行すれば、&lt;code&gt;path/to/static/ogp&lt;/code&gt;直下に、&lt;code&gt;path/to/content/posts/*.md&lt;/code&gt;に該当する個々のmarkdownファイルから作成したOGP画像が出力されます。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;go install github.com/Ladicle/tcardgen@latest
# 出力先の画像のディレクトリは事前に作る必要がある
mkdir -p path/to/static/ogp
tcardgen \
--fontDir path/to/fontDir \
--output path/to/static/ogp \
--template path/to/templateFile \
path/to/content/posts/*.md
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;ここで、出力される画像のファイル名は、Markdownファイルのファイル名と同じものになります。index.mdならindex.pngです。Leaf Bundleでは、記事のmarkdownファイルのファイル名は全てindex.mdのため、全ての記事のOGP画像はindex.pngとなり、ファイルが上書きされて1個しか生成されません。当たり前と言えば当たり前なのですが、ここでハマりました。&lt;/p&gt;
&lt;p&gt;これを解消するために、&lt;code&gt;content/posts/yyyymmdd-hoge-fuga-piyo/index.md&lt;/code&gt;のOGP画像は&lt;code&gt;hoge-fuga-piyo.png&lt;/code&gt;とするように、1ファイルずつtcardgenを走らせます。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;go install github.com/Ladicle/tcardgen@latest
mkdir -p static/ogp
files=`find ./content/posts/*/index.md -type f`
for f in $files; do
slugname=`dirname ${f} | xargs -I@ basename @ | cut -c 10-`
tcardgen --fontDir ogp/font --output &amp;quot;static/ogp/${slugname}.png&amp;quot; \
--template ogp/tcard/template.png ${f}
done
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;このシェルスクリプトで&lt;code&gt;content/posts/yyyymmdd-&amp;lt;適当な名前&amp;gt;/index.md&lt;/code&gt;に合致する全てのmarkdownファイルのOGP画像が&lt;code&gt;static/ogp/&amp;lt;適当な名前&amp;gt;.png&lt;/code&gt;に出力されます。&lt;/p&gt;
&lt;p&gt;ローカルで実行すればローカルで画像が生成されますが、今回はGitHub Actions上で行います。あとで説明します。&lt;/p&gt;
&lt;h3 id="hugoのテンプレートにogpを入れる"&gt;HugoのテンプレートにOGPを入れる&lt;/h3&gt;
&lt;p&gt;Step1でOGP画像は出力されますが、Hugoのテーマで対応していなければURLが貼られたときにOGP画像が表示されません。&lt;/p&gt;
&lt;p&gt;OGP画像とOGPのdescriptionが表示されるよう、HugoのAnubisというThemeの&lt;code&gt;footer.html&lt;/code&gt;に以下を追加しました&lt;sup id="fnref:2"&gt;&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref"&gt;2&lt;/a&gt;&lt;/sup&gt;。テーマ側でOGPに対応していなければどのテーマでも自分で書くことになります。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-html"&gt;&amp;lt;meta property=&amp;quot;og:url&amp;quot; content=&amp;quot;{{ .Permalink }}&amp;quot; /&amp;gt;
&amp;lt;meta property=&amp;quot;og:type&amp;quot; content=&amp;quot;{{ if .IsHome }}website{{ else }}article{{ end }}&amp;quot; /&amp;gt;
&amp;lt;meta property=&amp;quot;og:site_name&amp;quot; content=&amp;quot;{{ .Site.Title }}&amp;quot; /&amp;gt;
&amp;lt;meta property=&amp;quot;og:title&amp;quot; content=&amp;quot;{{ .Title }}&amp;quot; /&amp;gt;
&amp;lt;meta property=&amp;quot;og:description&amp;quot; content=&amp;quot;{{ with .Description -}}{{ . }}{{ else -}}{{ if .IsPage }}{{ substr .Summary 0 300 }}{{ else }}{{ with .Site.Params.description }}{{ . }}{{ end }}{{ end }}{{ end }}&amp;quot; /&amp;gt;
&amp;lt;meta property=&amp;quot;og:image&amp;quot; content=&amp;quot;{{ if .Params.thumbnail -}}{{ .Params.thumbnail|absURL }}{{ else if and .IsPage (eq .Section &amp;quot;posts&amp;quot;)}}{{ path.Join &amp;quot;ogp&amp;quot; (print .Slug &amp;quot;.png&amp;quot;) | absURL }}{{ else -}}{{ &amp;quot;img/default.png&amp;quot; | absURL }}{{ end -}}&amp;quot; /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;posts/yyyymmdd-hoge-fuga-piyo/index.md&lt;/code&gt;のOGP画像として、&lt;code&gt;ogp/{slug}.png&lt;/code&gt;が読み込まれます。&lt;/p&gt;
&lt;p&gt;このため、各記事のindex.mdのslugを&lt;code&gt;hoge-fuga-piyo&lt;/code&gt;にする必要があります。ディレクトリ名の&lt;code&gt;yyyymmdd-hoge-fuga-piyo&lt;/code&gt;の&lt;code&gt;hoge-fuga-piyo&lt;/code&gt;とslugを一致させる必要があるということです。実装的に微妙な気はしますがこれで困らないのでいいかなと…。&lt;/p&gt;
&lt;p&gt;なお、OGP画像の下に展開されるdescriptionは、各index.mdのFront Matterに&lt;code&gt;description&lt;/code&gt;を書いていればそれが、書いていなければFront Matterの直下から最初の300文字になります。&lt;/p&gt;
&lt;h3 id="github-actionsのyamlファイルの作成"&gt;GitHub Actionsのyamlファイルの作成&lt;/h3&gt;
&lt;p&gt;以上をGitHub Actionsにするとこうなります。&lt;code&gt;.github/workflows&lt;/code&gt;直下に拡張子&lt;code&gt;.yml&lt;/code&gt;で任意のファイル名で保存します。&lt;/p&gt;
&lt;p&gt;OGP生成に必要なのは&lt;code&gt;Setup Go&lt;/code&gt;と&lt;code&gt;Install tcardgen&lt;/code&gt;と&lt;code&gt;Generate ogp images&lt;/code&gt;です。他は通常のHugoのビルド部分とGitHub Pagesへのpush部分なので必要に応じて変更してください。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-yaml"&gt;name: github-pages
on:
push:
branches:
- master
jobs:
deploy:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v2
with:
submodules: true # Fetch Hugo themes (true OR recursive)
fetch-depth: 0 # Fetch all history for .GitInfo and .Lastmod
- name: Setup Go
uses: actions/setup-go@v4
with:
go-version: '1.21.1'
- name: Install tcardgen
run: go install github.com/Ladicle/tcardgen
- name: Generate ogp images
run: |
mkdir -p static/ogp
files=`find ./content/posts/*/index.md -type f`
for f in $files; do
slugname=`dirname ${f} | xargs -I@ basename @ | cut -c 10-`
tcardgen --fontDir ogp/font --output &amp;quot;static/ogp/${slugname}.png&amp;quot; \
--template ogp/tcard/template.png ${f}
done
- name: Setup Hugo
uses: peaceiris/actions-hugo@v2
with:
hugo-version: 'latest'
# extended: true
- name: Build
run: hugo
- name: Deploy
uses: peaceiris/actions-gh-pages@v3
with:
deploy_key: ${{ secrets.ACTIONS_DEPLOY_KEY }}
external_repository: suzuna/blog
publish_dir: ./docs
publish_branch: main
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id="tcardgenを改造する版"&gt;tcardgenを改造する版&lt;/h2&gt;
&lt;p&gt;わたしはtcardgenを改造しましたので、上と少し違ったGitHub Actionsを動かしています。&lt;/p&gt;
&lt;p&gt;tcardgenは、HugoのyamlのFront MatterのTitleだけでなく、CategoryとTag, Author, DateをOGP画像に含めます。&lt;/p&gt;
&lt;p&gt;しかし個人的にCategory, Tag, Author, DateはOGP画像に含めなくていいと思ったのと、Authorの代わりにブログのタイトルを表示したかったので、tcardgenをforkしてソースをいじって実現しました。Goは全く分からないのでソースは汚いです。&lt;/p&gt;
&lt;p&gt;本家のインストールのこちらの代わりに、&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;go install github.com/Ladicle/tcardgen@latest
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;自作のfork版をインストールします。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;go install github.com/suzuna/tcardgen@latest
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;forkしてGoを書いていじれる人はこんな記事を見なくてもできると思いますが…&lt;/p&gt;
&lt;p&gt;自作のfork版を使っているため、GitHub Actionsの&lt;code&gt;Install tcardgen&lt;/code&gt;と&lt;code&gt;Generate ogp images&lt;/code&gt;はさきほど紹介したものの代わりにこちらを使っています。&lt;code&gt;Generate ogp images&lt;/code&gt;のtcardgenの実行部分に&lt;code&gt;--topTitle&lt;/code&gt;や&lt;code&gt;--bottomAuthor&lt;/code&gt;というオプションが付いているのは独自仕様です。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-yaml"&gt;- name: Setup Go
uses: actions/setup-go@v4
with:
go-version: '1.21.1'
- name: Install tcardgen
run: go install github.com/suzuna/tcardgen
- name: Generate ogp images
run: |
mkdir -p static/ogp
files=`find ./content/posts/*/index.md -type f`
for f in $files; do
slugname=`dirname ${f} | xargs -I@ basename @ | cut -c 10-`
tcardgen --fontDir ogp/font --output &amp;quot;static/ogp/${slugname}.png&amp;quot; \
--template ogp/tcard/template.png --topTitle &amp;quot;&amp;quot; --bottomAuthor &amp;quot;suzuna's memo&amp;quot; ${f}
done
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id="参考"&gt;参考&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://gohugo.io/content-management/page-bundles/" target="_blank" rel="noopener noreferrer"&gt;Page bundles | Hugo&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://cpx.business/articles/file-structure-for-mastering-hugo/" target="_blank" rel="noopener noreferrer"&gt;[Hugo]Hugoを使いこなすためのオススメファイル構造 - CPX&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="footnotes" role="doc-endnotes"&gt;
&lt;hr&gt;
&lt;ol&gt;
&lt;li id="fn:1"&gt;
&lt;p&gt;うずらフォントもですが、BoldやMediumがないフォントの場合は、-Bold, -Mediumをファイル名につけた全く同じフォントを置く必要があります。tcardgenの内部でファイル名をパースしているからです。もちろん、そのようにしてBoldやMediumがないフォントを使うと、OGP画像のフォントの太さは当然すべて同じになります。&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:2"&gt;
&lt;p&gt;footer.htmlは&lt;code&gt;themes/hugo-theme-anubis/layouts/partials/footer.html&lt;/code&gt;にあるので、実際には、これをプロジェクトルートディレクトリ直下の&lt;code&gt;layouts/partials/footer.html&lt;/code&gt;にコピーし、そのコピーしたファイルに追加しました。&lt;code&gt;theme/&amp;lt;テーマ名&amp;gt;/layouts/&lt;/code&gt;以下のファイルを&lt;code&gt;layouts&lt;/code&gt;以下のファイルで上書きできるので、テーマを編集する場合は後者を変更します。そうしないとテーマ本体のアップデートによって編集内容が上書きされてしまいます。&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</description></item><item><title>階層ベイズで東京23区のお部屋の家賃相場を推定する</title><link>https://suzunano.net/posts/rent-modeling/</link><pubDate>Mon, 25 Dec 2023 00:00:00 +0900</pubDate><guid>https://suzunano.net/posts/rent-modeling/</guid><description>&lt;h2 id="はじめに"&gt;はじめに&lt;/h2&gt;
&lt;p&gt;この記事は&lt;a href="https://qiita.com/advent-calendar/2023/ppl" target="_blank" rel="noopener noreferrer"&gt;確率的プログラミング言語 Advent Calendar
2023&lt;/a&gt;の19日目の記事です。&lt;a href="https://qiita.com/hankagosa" target="_blank" rel="noopener noreferrer"&gt;松浦先生&lt;/a&gt;、主催いただきありがとうございます。今回は参加できてうれしく思います。6日遅れての投稿で恐縮ですがどうぞよろしくお願いいたします。&lt;/p&gt;
&lt;p&gt;SUUMOからスクレイピングした東京23区の20万件程度の賃貸物件のデータを用いて、最寄り駅で階層化した家賃の階層モデルをStanで実装して家賃相場を推定してみました。&lt;/p&gt;
&lt;p&gt;スクレイピングはPython + requests +
BeautifulSoup4、それ以降の分析パートはR + Stan (rstan)
で実装しました。Stanの可視化にはbayesplotやtidybayesを使っています。&lt;/p&gt;
&lt;p&gt;データ取得や前処理、可視化の部分がだいぶ長くなってしまいましたので、モデルの推定や結果については「モデリング」の章からご覧ください。&lt;/p&gt;
&lt;p&gt;追記: 続きの記事を書きました。&lt;a href="../rent-by-floor/"&gt;部屋の階数は家賃にどれだけ影響を与えるのか？ - suzuna's memo&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="モデリングの動機"&gt;モデリングの動機&lt;/h2&gt;
&lt;p&gt;部屋を探すとき、都心に住みたいけど高いなあ、じゃあこの路線沿いで都心から離れるほどどのくらい家賃相場が変わるの？とか、新築は高いなあ、築年数を広げるとどのくらい家賃が下がるの？とか、一度は考えたことはないでしょうか？&lt;/p&gt;
&lt;p&gt;何々駅の家賃相場は1LDKでいくらという情報はググれば見つけられます。大まかな家賃相場を理解するために有用な情報なのですが、築年数や駅からの距離によって当然相場は変わってきます。築浅がいい、逆に古くてもいいから安くしたいとか、絶対駅近がいい、駅から離れていてもいいとか、人によって好みがあります。&lt;/p&gt;
&lt;p&gt;また、築浅物件が多い街、そうではない街という違いもあります。単純に平均すると、築年数の影響で後者の街の家賃相場が下がってしまうので、築年数の影響を除いた相場も知りたいですね&lt;sup id="fnref:1"&gt;&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref"&gt;1&lt;/a&gt;&lt;/sup&gt;。&lt;/p&gt;
&lt;p&gt;それに、例えば1LDKと言っても面積は物件によって様々です。40m2の1LDKと50m2の1LDKでは、他の条件が同じなら当然後者の方が高いです。なので、例えば最寄り駅がここで面積はいくつで築年数が何年で徒歩何分の物件はいくらかという情報が知りたくなるところです。&lt;/p&gt;
&lt;p&gt;直感的には、同じ最寄り駅の物件に限定すると、家賃は面積の単調増加、築年数と最寄り駅からの徒歩分数の単調減少な関数であり、地価の違いからこの回帰直線は物件の最寄り駅によって上下動するように思われます。階層ベイズにぴったりのテーマのように思われます。&lt;/p&gt;
&lt;p&gt;わたしが知りたいのは以下の内容です。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;築年数が1年増えると家賃がどの程度下がるのか&lt;/li&gt;
&lt;li&gt;駅からの徒歩分数が1分増えると家賃がどの程度下がるのか&lt;/li&gt;
&lt;li&gt;築年数、徒歩分数、面積を固定したとき、最寄り駅によってどの程度家賃相場が変わるのか&lt;/li&gt;
&lt;li&gt;面積を変えるとどの程度家賃相場が変わるのか&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;これが分かれば、任意の築年数、徒歩分数、面積、最寄り駅での家賃相場を推定することができます。&lt;/p&gt;
&lt;p&gt;というわけで、自分が知りたくなったのでStanで実装してみました。&lt;/p&gt;
&lt;h2 id="環境"&gt;環境&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;R 4.3.1&lt;/li&gt;
&lt;li&gt;rstan 2.32.3&lt;/li&gt;
&lt;li&gt;bayesplot 1.10.0&lt;/li&gt;
&lt;li&gt;tidybayes 3.0.6&lt;/li&gt;
&lt;li&gt;furrr 0.3.1&lt;/li&gt;
&lt;li&gt;MLmetrics 1.1.1&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="問題設定"&gt;問題設定&lt;/h2&gt;
&lt;h3 id="目的変数と説明変数"&gt;目的変数と説明変数&lt;/h3&gt;
&lt;p&gt;対象は東京23区の賃貸物件です。&lt;/p&gt;
&lt;p&gt;目的変数を家賃+管理費とするモデルを組むことにします。要するに毎月発生する費用です。敷金や礼金は考慮しないこととします&lt;sup id="fnref:2"&gt;&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref"&gt;2&lt;/a&gt;&lt;/sup&gt;。以下断りがない限り「家賃」は「家賃+管理費」を指します。&lt;/p&gt;
&lt;p&gt;使える説明変数は、SUUMOでスクレイピングした物件リストのページに存在する以下の項目です。今回のモデリングではこの中でも使っていない変数もあります。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;住所（「東京都千代田区千代田1」の粒度まで）&lt;/li&gt;
&lt;li&gt;専有面積&lt;/li&gt;
&lt;li&gt;築年数&lt;/li&gt;
&lt;li&gt;最寄り駅の路線、駅名、駅からの徒歩の分数
&lt;ul&gt;
&lt;li&gt;最大3つの最寄り駅が記載されている&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;間取り（1LDKとか）&lt;/li&gt;
&lt;li&gt;建物の高さ（地上x階地下y階建てのxとy）&lt;/li&gt;
&lt;li&gt;部屋の階数&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;敷金や礼金も取得できますが、説明変数には使用しません。敷金や礼金はたいてい家賃の0～2ヶ月分なので、これを説明変数に入れると家賃+管理費はある程度予測できてしまうからです。ある意味Leakageですね。&lt;/p&gt;
&lt;p&gt;SUUMOの物件ごとのページには以下の項目も記載されていますが、このページもスクレイピングしようとするとスクレイピングにかかる時間が膨れ上がるため、今回はスクレイピングせず使用しませんでした。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;部屋の特徴・設備
&lt;ul&gt;
&lt;li&gt;バストイレ別や浴室乾燥機があるかどうか、角部屋かどうかなど&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;部屋の方角&lt;/li&gt;
&lt;li&gt;建物の構造（鉄筋や鉄骨など）&lt;/li&gt;
&lt;li&gt;近所のコンビニやスーパーマーケットなどの店名と物件からそこまでの距離&lt;/li&gt;
&lt;li&gt;部屋の画像&lt;/li&gt;
&lt;li&gt;敷金・礼金以外のその他の初期費用&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="分析の流れ"&gt;分析の流れ&lt;/h3&gt;
&lt;p&gt;以下の流れで進めていきます。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;データの取得（SUUMOのスクレイピング）&lt;/li&gt;
&lt;li&gt;前処理&lt;/li&gt;
&lt;li&gt;可視化（探索的データ分析）&lt;/li&gt;
&lt;li&gt;可視化の結果をもとにモデルを定式化&lt;/li&gt;
&lt;li&gt;MCMCでのモデルのパラメータ推定&lt;/li&gt;
&lt;li&gt;パラメータが正しく推定できているかチェック&lt;/li&gt;
&lt;li&gt;推定結果の解釈&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="データ取得"&gt;データ取得&lt;/h2&gt;
&lt;p&gt;物件ごとの家賃と面積などのデータを&lt;a href="https://suumo.jp/kanto/" target="_blank" rel="noopener noreferrer"&gt;SUUMO&lt;/a&gt;から2023年11月にスクレイピングしました&lt;sup id="fnref:3"&gt;&lt;a href="#fn:3" class="footnote-ref" role="doc-noteref"&gt;3&lt;/a&gt;&lt;/sup&gt;。&lt;/p&gt;
&lt;p&gt;例えば、東京都千代田区の全ての物件は、&lt;a href="https://suumo.jp/jj/chintai/ichiran/FR301FC001/?ar=030&amp;amp;bs=040&amp;amp;ta=13&amp;amp;sc=13101&amp;amp;cb=0.0&amp;amp;ct=9999999&amp;amp;mb=0&amp;amp;mt=9999999&amp;amp;et=9999999&amp;amp;cn=9999999&amp;amp;shkr1=03&amp;amp;shkr2=03&amp;amp;shkr3=03&amp;amp;shkr4=03&amp;amp;sngz=&amp;amp;po1=25&amp;amp;pc=50" target="_blank" rel="noopener noreferrer"&gt;こちらのページ&lt;/a&gt;で見ることができます。このページを23区分、PythonのrequestsとBeautifulSoup4を用いてスクレイピングしました。一つの区について、ページネーションを1ページずつめくっていきます。1ページに50件物件が載っているので意外とサクサク取得できます。&lt;/p&gt;
&lt;p&gt;数時間かけて129978件の建物における243544件の物件情報を収集しました。ただし、同じ物件が異なる建物名の部屋として重複して登録されていることがあり、その重複を除くと実際には211074件の物件となりました。&lt;/p&gt;
&lt;p&gt;Rでスクレイピングしてもよかったですが、Pythonを使ったのは今回はPythonでのクラスや例外処理の勉強を兼ねたためでもあります。rvestも超優秀なパッケージです。&lt;/p&gt;
&lt;p&gt;なお、上の一覧ページから各物件の詳細ページに飛ぶと、部屋の設備の有無などの詳細な情報を取得することができます。しかし、このページは物件の数だけページが存在するためにスクレイピングにかかる時間が長くなってしまうのでスクレイピングしていません。前のページが1ページに50件載っているのに対してこちらは1ページに1件なので、こちらも取得しようとするとさらに50倍の時間がかかります。&lt;/p&gt;
&lt;/details&gt;
&lt;p&gt;出力した結果のjsonファイルはこんな感じです。長いので折り畳んでいます。内容はダミーです。建物の数だけ”name”があり、“room”は同一の物件内の部屋の数だけあります。&lt;/p&gt;
&lt;details&gt;
&lt;summary&gt;
jsonの中身
&lt;/summary&gt;
&lt;pre&gt;&lt;code class="language-json"&gt;[
{
&amp;quot;type&amp;quot;: &amp;quot;賃貸マンション&amp;quot;,
&amp;quot;name&amp;quot;: &amp;quot;hogehogeマンション&amp;quot;,
&amp;quot;address&amp;quot;: &amp;quot;東京都千代田区千代田１&amp;quot;,
&amp;quot;moyorieki&amp;quot;: [
&amp;quot;東京メトロ千代田線/大手町駅 歩5分&amp;quot;,
&amp;quot;東京メトロ日比谷線/日比谷駅 歩6分&amp;quot;,
&amp;quot;ＪＲ中央線/東京駅 歩10分&amp;quot;
],
&amp;quot;age&amp;quot;: &amp;quot;築1年&amp;quot;,
&amp;quot;story&amp;quot;: &amp;quot;10階建&amp;quot;,
&amp;quot;room&amp;quot;: [
{
&amp;quot;floor&amp;quot;: &amp;quot;3階&amp;quot;,
&amp;quot;rent&amp;quot;: &amp;quot;10万円&amp;quot;,
&amp;quot;admin&amp;quot;: &amp;quot;5000円&amp;quot;,
&amp;quot;shikikin&amp;quot;: &amp;quot;10万円&amp;quot;,
&amp;quot;reikin&amp;quot;: &amp;quot;-&amp;quot;,
&amp;quot;layout&amp;quot;: &amp;quot;ワンルーム&amp;quot;,
&amp;quot;area&amp;quot;: &amp;quot;25.05m2&amp;quot;,
&amp;quot;link&amp;quot;: &amp;quot;&amp;lt;物件ページへのリンク&amp;gt;&amp;quot;
},
{
&amp;quot;floor&amp;quot;: &amp;quot;5階&amp;quot;,
&amp;quot;rent&amp;quot;: &amp;quot;12.5万円&amp;quot;,
&amp;quot;admin&amp;quot;: &amp;quot;10000円&amp;quot;,
&amp;quot;shikikin&amp;quot;: &amp;quot;12.5万円&amp;quot;,
&amp;quot;reikin&amp;quot;: &amp;quot;12.5&amp;quot;,
&amp;quot;layout&amp;quot;: &amp;quot;1LDK&amp;quot;,
&amp;quot;area&amp;quot;: &amp;quot;50.50m2&amp;quot;,
&amp;quot;link&amp;quot;: &amp;quot;&amp;lt;物件ページへのリンク&amp;gt;&amp;quot;
}
]
},
{
&amp;quot;type&amp;quot;: &amp;quot;賃貸マンション&amp;quot;,
（以下同様）
}
]
&lt;/code&gt;&lt;/pre&gt;
&lt;/details&gt;
&lt;h2 id="前処理"&gt;前処理&lt;/h2&gt;
&lt;p&gt;ここからはRで行います。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-r"&gt;library(tidyverse)
library(rstan)
library(bayesplot)
library(tidybayes)
library(patchwork)
library(furrr)
library(MLmetrics)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上の章で出力したjsonファイルを&lt;code&gt;data.frame&lt;/code&gt;で読み込んで&lt;code&gt;df&lt;/code&gt;という変数に入れておきます。&lt;/p&gt;
&lt;p&gt;まず建物名以外が全く同じで建物名のみ異なる物件が結構あります。これは重複とみなして&lt;code&gt;dplyr::distinct()&lt;/code&gt;でレコードを削除しています（32000件程度）。そのうえでいくつかの条件で物件を除外しています。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;1.1個目の最寄り駅から徒歩ではない物件（バスや車） [1025件]
&lt;ul&gt;
&lt;li&gt;徒歩以外を考慮しようとすると手間がかかるから&lt;/li&gt;
&lt;li&gt;この除外により、最寄駅からバスや車という物件が多い駅の家賃を過大に評価しそうだが今回は単純に除外した&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;2.賃貸マンション、賃貸アパート以外の物件（一戸建てなど）[5392件]
&lt;ul&gt;
&lt;li&gt;マンションやアパートとは同列に扱えないから&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;3.物件の階数の情報がない物件 [11件]
&lt;ul&gt;
&lt;li&gt;“-”のようなパターン&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;4.物件の階数が建物の地上階の階数より高い物件、または地下階の高さより低い物件
[189件]
&lt;ul&gt;
&lt;li&gt;誤入力？&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;次に以下のロジックで前処理しました。正規表現とstringrで何とかしました。前処理あるあるなんですが、このコードのロジックで漏れなく前処理できているか？というのを逐一確かめながらコードを書いていくのが大変なんですよね。それでもSUUMOのデータはかなりきれいでした。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ward&lt;/code&gt;（区）: &lt;code&gt;address&lt;/code&gt;（住所）から正規表現で区を抽出&lt;/li&gt;
&lt;li&gt;&lt;code&gt;area_str&lt;/code&gt;（面積）: “m2”を削除&lt;/li&gt;
&lt;li&gt;&lt;code&gt;age_str&lt;/code&gt;（築年数）:
「築x年」のxを抽出。「新築」なら0、「築99年以上」なら99とする&lt;/li&gt;
&lt;li&gt;&lt;code&gt;moyorieki_1&lt;/code&gt;（1個目の最寄り駅）から路線部分と駅名部分と徒歩x分のxをそれぞれ別のカラムに入れる&lt;/li&gt;
&lt;li&gt;&lt;code&gt;rent&lt;/code&gt;（家賃）, &lt;code&gt;admin&lt;/code&gt;（管理費）, &lt;code&gt;shikikin&lt;/code&gt;（敷金）,
&lt;code&gt;reikin&lt;/code&gt;（礼金）を万円単位で統一する。“-”は0とする&lt;/li&gt;
&lt;li&gt;目的変数である家賃+管理費を&lt;code&gt;rent_admin&lt;/code&gt;（万円）として列を足す&lt;/li&gt;
&lt;li&gt;&lt;code&gt;story&lt;/code&gt;（建物の階数）から地上階数と地下階数をそれぞれ&lt;code&gt;story_above&lt;/code&gt;と&lt;code&gt;story_under&lt;/code&gt;として取り出す。平屋は地上1階地下0階建てとする&lt;/li&gt;
&lt;li&gt;&lt;code&gt;floor&lt;/code&gt;（部屋の階数）から階数を取り出す
&lt;ul&gt;
&lt;li&gt;地下階はマイナスを付ける（例: “B2階”は-2とする）&lt;/li&gt;
&lt;li&gt;“1-2階”のような複数階にまたがるものは左側を採用（この例では1階とする）&lt;/li&gt;
&lt;li&gt;“3-1階”のようなパターンもあったが機械的に3階とした&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;適宜double型やinteger型に変換&lt;/li&gt;
&lt;/ul&gt;
&lt;details&gt;
&lt;summary&gt;前処理のコード&lt;/summary&gt;
&lt;pre&gt;&lt;code class="language-r"&gt;df2 &amp;lt;- df |&amp;gt;
# バスや車を除外
filter(str_detect(moyorieki_1, &amp;quot;駅 歩&amp;quot;)) |&amp;gt;
filter(!str_detect(moyorieki_1, &amp;quot;駅 バス&amp;quot;)) |&amp;gt;
# 「賃貸その他」、「賃貸テラス・タウンハウス」、「賃貸一戸建て」を除外
filter(type %in% c(&amp;quot;賃貸マンション&amp;quot;, &amp;quot;賃貸アパート&amp;quot;)) |&amp;gt;
# 階が入っていないものを除外
filter(floor_str != &amp;quot;-&amp;quot;)
df3 &amp;lt;- df2 |&amp;gt;
mutate(
ward=str_extract(address, &amp;quot;(?&amp;lt;=東京都).*?区&amp;quot;)
) |&amp;gt;
mutate(
area=as.numeric(str_remove(area, &amp;quot;m2$&amp;quot;))
) |&amp;gt;
mutate(
age=case_when(
age_str == &amp;quot;新築&amp;quot; ~ 0L,
age_str == &amp;quot;築99年以上&amp;quot; ~ 99L,
str_detect(age_str, &amp;quot;^築[0-9]{1,2}年$&amp;quot;) ~ as.integer(str_extract(age_str, &amp;quot;(?&amp;lt;=築)[0-9]{1,2}(?=年)&amp;quot;))
)
) |&amp;gt;
mutate(
moyorieki_1_railroad=str_extract(moyorieki_1, &amp;quot;^.*(?=/)&amp;quot;),
moyorieki_1_station=str_extract(moyorieki_1, &amp;quot;(?&amp;lt;=/).*駅&amp;quot;),
moyorieki_1_walk=as.integer(str_extract(moyorieki_1, &amp;quot;(?&amp;lt;=駅 歩).*(?=分)&amp;quot;))
) |&amp;gt;
mutate(
across(
c(rent, admin, shikikin, reikin),
~{
case_when(
.x == &amp;quot;-&amp;quot; ~ 0L,
str_detect(.x, &amp;quot;万円$&amp;quot;) ~ as.numeric(str_extract(.x, &amp;quot;[0-9\\.]+(?=万円$)&amp;quot;)),
str_detect(.x, &amp;quot;(?!=万)円$&amp;quot;) ~ as.numeric(str_extract(.x, &amp;quot;[0-9\\.]+(?=円$)&amp;quot;))/10000
)
}
),
rent_admin=rent + admin
) |&amp;gt;
mutate(
story_above=case_when(
str_detect(story_str, &amp;quot;(?&amp;lt;=地上)[0-9]+(?=階建$)&amp;quot;) ~ as.integer(str_extract(story_str, &amp;quot;(?&amp;lt;=地上)[0-9]+(?=階建$)&amp;quot;)),
str_detect(story_str, &amp;quot;[0-9]+階建$&amp;quot;) ~ as.integer(str_extract(story_str, &amp;quot;[0-9]+(?=階建$)&amp;quot;)),
story_str == &amp;quot;平屋&amp;quot; ~ 1L
),
story_under=case_when(
str_detect(story_str, &amp;quot;(?&amp;lt;=^地下)[0-9]+&amp;quot;) ~ as.integer(str_extract(story_str, &amp;quot;(?&amp;lt;=^地下)[0-9]+&amp;quot;)),
story_str == &amp;quot;平屋&amp;quot; ~ 0L,
TRUE ~ 0L
)
) |&amp;gt;
mutate(
floor=case_when(
str_detect(floor_str, &amp;quot;^B[0-9]+階$&amp;quot;) ~ as.integer(str_extract(floor_str, &amp;quot;[0-9]+&amp;quot;)) * -1L,
floor_str == &amp;quot;B階&amp;quot; ~ -1L,
str_detect(floor_str, &amp;quot;B\\-.*階&amp;quot;) ~ -1L,
str_detect(floor_str, &amp;quot;^[0-9]+階$&amp;quot;) ~ as.integer(str_extract(floor_str, &amp;quot;[0-9]+&amp;quot;)),
str_detect(floor_str, &amp;quot;^B[0-9]+\\-[BM]?[0-9]+階$&amp;quot;) ~ as.integer(str_extract(floor_str, &amp;quot;(?&amp;lt;=B)[0-9]+&amp;quot;)) * -1L,
str_detect(floor_str, &amp;quot;^[0-9]+\\-[BM]?[0-9]+階$&amp;quot;) ~ as.integer(str_extract(floor_str, &amp;quot;^[0-9]+&amp;quot;))
)
)
df4 &amp;lt;- df3 |&amp;gt;
filter(floor &amp;gt;= story_under * -1, floor &amp;lt;= story_above) |&amp;gt;
select(
-moyorieki_1, -moyorieki_2, -moyorieki_3,
-age_str, -story_str, -floor_str,
-shikikin, -reikin
)
&lt;/code&gt;&lt;/pre&gt;
&lt;/details&gt;
&lt;p&gt;これを&lt;code&gt;df4&lt;/code&gt;として格納します。次の画像のようなdata.frameです。結果として204457件の物件が残りました。&lt;/p&gt;
&lt;img src="df.png" height="400"&gt;
&lt;h2 id="可視化"&gt;可視化&lt;/h2&gt;
&lt;p&gt;ようやくきれいなテーブルデータができたので、ggplot2で色々プロットしてみます。&lt;/p&gt;
&lt;p&gt;探索的データ分析の過程ではデュアルディスプレイの片面にRStudioとかVSCodeとかのIDE、もう片面にプロットを表示すると快適です。&lt;/p&gt;
&lt;h3 id="間取り面積の分布"&gt;間取り・面積の分布&lt;/h3&gt;
&lt;p&gt;1つの建物に3つの部屋の募集が掲載されていたら、物件数は3とカウントします。また、面積は150m2以上は150m2として扱います。&lt;/p&gt;
&lt;details&gt;
&lt;summary&gt;Code&lt;/summary&gt;
&lt;pre&gt;&lt;code class="language-r"&gt;p1 &amp;lt;- df4 |&amp;gt;
count(layout, name=&amp;quot;count&amp;quot;) |&amp;gt;
arrange(desc(count)) |&amp;gt;
ggplot(aes(forcats::fct_reorder(layout, count), count))+
theme_light()+
geom_bar(stat=&amp;quot;identity&amp;quot;, alpha=0.6, color=&amp;quot;black&amp;quot;)+
labs(x=&amp;quot;layout（間取り）&amp;quot;, y=&amp;quot;物件数（部屋）&amp;quot;)+
coord_flip()
p2 &amp;lt;- df4 |&amp;gt;
mutate(area=if_else(area &amp;gt;= 150, 150, area)) |&amp;gt;
ggplot(aes(area))+
theme_light()+
geom_histogram()+
scale_x_continuous(breaks=seq(0, 150, 50), minor_breaks=seq(0, 150, 10))+
labs(x=&amp;quot;area（面積）&amp;quot;, y=&amp;quot;物件数（部屋）&amp;quot;)
patchwork::wrap_plots(p1, p2, ncol=2)
&lt;/code&gt;&lt;/pre&gt;
&lt;/details&gt;
&lt;img src="images/image1-1.png" style="width:80.0%" /&gt;
&lt;p&gt;11Kや14Kは1Kの誤入力でした。20m2～50m2あたりのゾーンがボリュームゾーンなんですね。&lt;/p&gt;
&lt;h3 id="家賃築年数の分布"&gt;家賃・築年数の分布&lt;/h3&gt;
&lt;p&gt;左の家賃のグラフですが、外れ値でプロットが見づらくなるので家賃100万円以下の物件のみに絞ります。&lt;/p&gt;
&lt;details&gt;
&lt;summary&gt;Code&lt;/summary&gt;
&lt;pre&gt;&lt;code class="language-r"&gt;p1 &amp;lt;- df4 |&amp;gt;
ggplot(aes(rent_admin))+
theme_light()+
geom_histogram(binwidth=2)+
coord_cartesian(xlim=c(0, 100))+
scale_x_continuous(breaks=seq(0, 100, 10), minor_breaks=NULL)+
labs(x=&amp;quot;rent_admin（家賃+管理費）&amp;quot;, y=&amp;quot;物件数（部屋）&amp;quot;)
p2 &amp;lt;- df4 |&amp;gt;
ggplot(aes(age))+
theme_light()+
geom_histogram()+
scale_x_continuous(breaks=seq(0, 100, 10), minor_breaks=NULL)+
labs(x=&amp;quot;age（築年数）&amp;quot;, y=&amp;quot;物件数（部屋）&amp;quot;)
patchwork::wrap_plots(p1, p2, ncol=2)
&lt;/code&gt;&lt;/pre&gt;
&lt;/details&gt;
&lt;img src="images/image2-1.png" style="width:80.0%" /&gt;
&lt;p&gt;家賃の分布は右に裾を引いた対数正規分布のような形をしています。&lt;/p&gt;
&lt;p&gt;右の築年数の分布は20年と40年手前に崖があります。建物が満たすべき耐震基準は建築基準法で定められており、1981年以前に建築確認申請が行われた建物は「旧耐震基準」、それ以降の建物は「新耐震基準」、2000年以降の建物は「2000年基準」が適用されるそうです。後ろほど耐震基準が高まります。新耐震基準適用開始から40年、2000年基準適用開始から20年くらいになるので、20年や40年を目途に建て替えられているのかもしれません（建築は特に詳しくないので違うかもしれません。単なる推測です）。&lt;/p&gt;
&lt;h3 id="面積と家賃の散布図間取りで色分け"&gt;面積と家賃の散布図（間取りで色分け）&lt;/h3&gt;
&lt;p&gt;点の色分けが分かりづらくなるので、1R, 1K, 1DK, 1LDK, 2K, 2DK,
2LDK以外の間取りをothersにまとめています。また、面積100m2以下、家賃50万円以下の物件のみをプロットします。&lt;/p&gt;
&lt;details&gt;
&lt;summary&gt;Code&lt;/summary&gt;
&lt;pre&gt;&lt;code class="language-r"&gt;df4 |&amp;gt;
mutate(
layout=if_else(layout %in% c(&amp;quot;1K&amp;quot;, &amp;quot;1R&amp;quot;, &amp;quot;1LDK&amp;quot;, &amp;quot;1DK&amp;quot;, &amp;quot;2LDK&amp;quot;, &amp;quot;2DK&amp;quot;, &amp;quot;2K&amp;quot;), layout, &amp;quot;others&amp;quot;)
) |&amp;gt;
ggplot(aes(area, rent_admin, color=layout))+
theme_light()+
geom_point(size=0.1)+
coord_cartesian(xlim=c(0, 100), ylim=c(0, 50))+
guides(colour=guide_legend(override.aes=list(size=6)))+
labs(x=&amp;quot;area（面積）&amp;quot;, y=&amp;quot;rent_admin（家賃+管理費）&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;/details&gt;
&lt;img src="images/image3-1.png" style="width:80.0%" /&gt;
&lt;p&gt;面積が小さい方から1R（緑色）、1K（黄土色）、1DK（オレンジ色）、1LDK（黄緑色）、2LDK（紫色）は同じ直線に乗っているように見えます。つまり、これらの5個の間取りに関しては、面積と間取りは相関が非常に高く多重共線性を起こしそうなことを示します。&lt;/p&gt;
&lt;p&gt;一方でちょっと見づらいですが、2K（青色）は1K～1DKと同じ面積なのに下の方に位置するように見えます。同じく2DK（水色）も1LDKと同じ面積なのに下の方にあります。&lt;/p&gt;
&lt;p&gt;この理由は次のプロットで推測できます。&lt;/p&gt;
&lt;h3 id="築年数の分布間取り別"&gt;築年数の分布（間取り別）&lt;/h3&gt;
&lt;p&gt;上の散布図と同じデータを、築年数のヒストグラムで描きます。&lt;/p&gt;
&lt;details&gt;
&lt;summary&gt;Code&lt;/summary&gt;
&lt;pre&gt;&lt;code class="language-r"&gt;df4 |&amp;gt;
mutate(
layout=if_else(layout %in% c(&amp;quot;1K&amp;quot;, &amp;quot;1R&amp;quot;, &amp;quot;1LDK&amp;quot;, &amp;quot;1DK&amp;quot;, &amp;quot;2LDK&amp;quot;, &amp;quot;2DK&amp;quot;, &amp;quot;2K&amp;quot;), layout, &amp;quot;others&amp;quot;)
) |&amp;gt;
ggplot(aes(age))+
theme_light()+
geom_histogram()+
labs(x=&amp;quot;age（築年数）&amp;quot;, y=&amp;quot;物件数（部屋）&amp;quot;)+
theme(
strip.background=element_rect(color=&amp;quot;black&amp;quot;, fill=&amp;quot;white&amp;quot;),
strip.text=element_text(color=&amp;quot;black&amp;quot;)
)+
facet_wrap(~layout, ncol=4)
&lt;/code&gt;&lt;/pre&gt;
&lt;/details&gt;
&lt;img src="images/image4-1.png" style="width:80.0%" /&gt;
&lt;p&gt;すると、2Kと2DKは他の間取りと違い、築年数が25年～50年の間にピークがあることが分かります。&lt;/p&gt;
&lt;p&gt;つまり、一つ前の面積と家賃の散布図における、「2Kは1Kや1DKと同じ面積なのに下の方に位置し、同様に2DKは1LDKと同じ面積なのに下の方に位置する」現象は、2Kと2DKは築年数が経過した物件が多いため、2Kは1Kや1DK、2DKは1LDKと同じ面積でも家賃が安いということだと推測できます。そうなら、間取りは説明変数から削除してもよさそうです。&lt;/p&gt;
&lt;p&gt;以下のページにもあるのですが、間取りにも流行りがあるそうです。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.homes.co.jp/cont/rent/rent_00274/" target="_blank" rel="noopener noreferrer"&gt;【ホームズ】お得に部屋を借りたい人は「2K」も視野に！ワンルームより家賃が安いことも
|
住まいのお役立ち情報&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.chintai.net/news/2015/02/12/3641/" target="_blank" rel="noopener noreferrer"&gt;1LDKと2DKはどっちが安い？間取りの違いとおすすめな人もご紹介 |
CHINTAI情報局&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="面積と家賃の散布図築年数別徒歩分数別"&gt;面積と家賃の散布図（築年数別・徒歩分数別）&lt;/h3&gt;
&lt;p&gt;上で見た面積と家賃のプロットを150m2以下、200万円以下に広げて築年数で色分けしてみます。グラデーションを見やすくするため、築年数が40年以上は40年としています。右のプロットは面積と家賃を両方対数を取ったものです。&lt;/p&gt;
&lt;details&gt;
&lt;summary&gt;Code&lt;/summary&gt;
&lt;pre&gt;&lt;code class="language-r"&gt;p1 &amp;lt;- df3 |&amp;gt;
filter(area &amp;lt;= 150 &amp;amp; rent_admin &amp;lt;= 200) |&amp;gt;
mutate(age=if_else(age &amp;gt;= 40L, 40L, age)) |&amp;gt;
ggplot(aes(area, rent_admin, color=age))+
theme_light()+
geom_point(size=0.1)+
theme(legend.position=&amp;quot;bottom&amp;quot;)+
labs(x=&amp;quot;area（面積）&amp;quot;, y=&amp;quot;rent_admin（家賃+管理費）&amp;quot;)+
geom_vline(xintercept=10, color=&amp;quot;purple&amp;quot;)+
geom_vline(xintercept=100, color=&amp;quot;black&amp;quot;)+
geom_hline(yintercept=100, color=&amp;quot;firebrick&amp;quot;)
p2 &amp;lt;- df3 |&amp;gt;
filter(area &amp;lt;= 150 &amp;amp; rent_admin &amp;lt;= 200) |&amp;gt;
mutate(age=if_else(age &amp;gt;= 40L, 40L, age)) |&amp;gt;
ggplot(aes(log(area), log(rent_admin), color=age))+
theme_light()+
geom_point(size=0.1)+
theme(legend.position=&amp;quot;bottom&amp;quot;)+
labs(x=&amp;quot;log(area)（面積の対数）&amp;quot;, y=&amp;quot;log(rent_admin)（家賃+管理費の対数）&amp;quot;)+
geom_vline(xintercept=log(10), color=&amp;quot;purple&amp;quot;)+
geom_vline(xintercept=log(100), color=&amp;quot;black&amp;quot;)+
geom_hline(yintercept=log(100), color=&amp;quot;firebrick&amp;quot;)
patchwork::wrap_plots(p1, p2, ncol=2)
&lt;/code&gt;&lt;/pre&gt;
&lt;/details&gt;
&lt;img src="images/image5-1.png" style="width:80.0%" /&gt;
&lt;p&gt;x=10, x=100, y=100にそれぞれ紫色、黒色、赤色の線を引いています。&lt;/p&gt;
&lt;p&gt;右側の両対数プロットだと、築年数が増えるほど点が下方にシフトしているように見えますね。&lt;/p&gt;
&lt;p&gt;左のプロットを見ると、面積が100m2（黒色の線）を超えるあたりからばらつきが大きくなっています。100m2を超える広い物件は家賃のメカニズムも変わってきそうです。23区で100m2のマンションはかなり広いです。面積がかなり大きいゾーンにも当てはめようとすると、ボリュームゾーンの25m2～50m2の物件の当てはまりが悪くなりそうなので、100m2を超える物件はモデルから除外することにします。また、100m2超はデータの誤入力っぽい物件もちらほらあるので、それを弾く意味もあります。&lt;/p&gt;
&lt;p&gt;100m2以下の物件はほとんど100万円（赤色の線）以下に収まっていますね。いま、モデルには100m2以下の物件データのみを使うことにしたので、誤入力をはじく意味で家賃が100万円の物件も除外することにします。&lt;/p&gt;
&lt;p&gt;また、右の対数のプロットを見ると、10m2（紫色の線）より左側は線形の関係が成り立っていなさそうです。このような狭い物件にも対数線形のモデルをフィットさせようとするのはやはり難しそうなので、10m2以下の物件も除外します。&lt;/p&gt;
&lt;p&gt;次のプロットは、先ほどのプロットの色分けを最寄り駅からの徒歩分数にしたものです。ただし20分以上は20分にまとめています。&lt;/p&gt;
&lt;details&gt;
&lt;summary&gt;Code&lt;/summary&gt;
&lt;pre&gt;&lt;code class="language-r"&gt;p1 &amp;lt;- df3 |&amp;gt;
filter(area &amp;lt;= 150 &amp;amp; rent_admin &amp;lt;= 200) |&amp;gt;
mutate(moyorieki_1_walk=if_else(moyorieki_1_walk &amp;gt;= 20L, 20L, moyorieki_1_walk)) |&amp;gt;
ggplot(aes(area, rent_admin, color=moyorieki_1_walk))+
theme_light()+
geom_point(size=0.1)+
theme(legend.position=&amp;quot;bottom&amp;quot;)+
labs(x=&amp;quot;area（面積）&amp;quot;, y=&amp;quot;rent_admin（家賃+管理費）&amp;quot;)+
geom_vline(xintercept=10, color=&amp;quot;purple&amp;quot;)+
geom_vline(xintercept=100, color=&amp;quot;black&amp;quot;)+
geom_hline(yintercept=100, color=&amp;quot;firebrick&amp;quot;)
p2 &amp;lt;- df3 |&amp;gt;
filter(area &amp;lt;= 150 &amp;amp; rent_admin &amp;lt;= 200) |&amp;gt;
mutate(moyorieki_1_walk=if_else(moyorieki_1_walk &amp;gt;= 20L, 20L, moyorieki_1_walk)) |&amp;gt;
ggplot(aes(log(area), log(rent_admin), color=moyorieki_1_walk))+
theme_light()+
geom_point(size=0.1)+
theme(legend.position=&amp;quot;bottom&amp;quot;)+
labs(x=&amp;quot;log(area)（面積の対数）&amp;quot;, y=&amp;quot;log(rent_admin)（家賃+管理費の対数）&amp;quot;)+
geom_vline(xintercept=log(10), color=&amp;quot;purple&amp;quot;)+
geom_vline(xintercept=log(100), color=&amp;quot;black&amp;quot;)+
geom_hline(yintercept=log(100), color=&amp;quot;firebrick&amp;quot;)
patchwork::wrap_plots(p1, p2, ncol=2)
&lt;/code&gt;&lt;/pre&gt;
&lt;/details&gt;
&lt;img src="images/image6-1.png" style="width:80.0%" /&gt;
&lt;p&gt;築年数と同様、徒歩分数が増えると点が下方にシフトしているように見えます。これは、対数家賃を対数面積で回帰するモデルを組むと、築年数と徒歩分数はこの回帰式の項に加えられることを示唆します。&lt;/p&gt;
&lt;h3 id="面積と家賃の散布図最寄り駅別築年数で色分け"&gt;面積と家賃の散布図（最寄り駅別・築年数で色分け）&lt;/h3&gt;
&lt;p&gt;23区の各区ごとに最も物件数が多い最寄り駅の物件について、築年数で色分けして面積と家賃の散布図を描いてみます。グラデーションを見やすくするため、築年数が40年以上は40年としています。面積は100m2以下、家賃は50万円以下に絞っています。&lt;/p&gt;
&lt;details&gt;
&lt;summary&gt;Code&lt;/summary&gt;
&lt;pre&gt;&lt;code class="language-r"&gt;top_stations_in_each_ward &amp;lt;- df4 |&amp;gt;
count(ward, moyorieki_1_station, name=&amp;quot;n&amp;quot;) |&amp;gt;
group_by(ward) |&amp;gt;
mutate(rank=dense_rank(desc(n))) |&amp;gt;
filter(rank == 1) |&amp;gt;
pull(moyorieki_1_station)
df4 |&amp;gt;
filter(moyorieki_1_station %in% top_stations_in_each_ward) |&amp;gt;
mutate(age=if_else(age &amp;gt;= 40L, 40L, age)) |&amp;gt;
ggplot(aes(area, rent_admin, color=age))+
theme_light()+
geom_point(size=1)+
coord_cartesian(xlim=c(0, 100), ylim=c(0, 50))+
labs(x=&amp;quot;area（面積）&amp;quot;, y=&amp;quot;rent_admin（家賃+管理費）&amp;quot;)+
theme(
strip.background=element_rect(color=&amp;quot;black&amp;quot;, fill=&amp;quot;white&amp;quot;),
strip.text=element_text(color=&amp;quot;black&amp;quot;)
)+
facet_wrap(~moyorieki_1_station, ncol=4)
&lt;/code&gt;&lt;/pre&gt;
&lt;/details&gt;
&lt;img src="images/image7-1.png" style="width:80.0%" /&gt;
&lt;p&gt;恵比寿と麻布十番は高いですね。神田や勝どきも高いし、濃い色の点が多いので築浅物件が多そうです（実際、駅別に築年数のヒストグラムを描くとその通りです）。&lt;/p&gt;
&lt;p&gt;次のプロットで面積と家賃をそれぞれ自然対数を取って描き直してみると、駅によって面積と家賃の傾きが違う直線に乗り、また対数を取る前より面積が大きいゾーン（x軸の右の方）のばらつきが抑えられたことが分かります。&lt;/p&gt;
&lt;details&gt;
&lt;summary&gt;Code&lt;/summary&gt;
&lt;pre&gt;&lt;code class="language-r"&gt;df4 |&amp;gt;
filter(moyorieki_1_station %in% top_stations_in_each_ward) |&amp;gt;
mutate(age=if_else(age &amp;gt;= 40L, 40L, age)) |&amp;gt;
ggplot(aes(log(area), log(rent_admin), color=age))+
theme_light()+
geom_point(size=1)+
labs(x=&amp;quot;log(area)（面積の対数）&amp;quot;, y=&amp;quot;log(rent_admin)（家賃+管理費の対数）&amp;quot;)+
theme(
strip.background=element_rect(color=&amp;quot;black&amp;quot;, fill=&amp;quot;white&amp;quot;),
strip.text=element_text(color=&amp;quot;black&amp;quot;)
)+
facet_wrap(~moyorieki_1_station, ncol=4)
&lt;/code&gt;&lt;/pre&gt;
&lt;/details&gt;
&lt;img src="images/image8-1.png" style="width:80.0%" /&gt;
&lt;h3 id="徒歩分数の分布"&gt;徒歩分数の分布&lt;/h3&gt;
&lt;p&gt;1個目の最寄り駅（物件ページの一番上に書いてある最寄り駅）からの徒歩分数の1分刻みのヒストグラムです。ただし30分以上の場合は30分としています。&lt;/p&gt;
&lt;p&gt;5分と10分が多く見えます。物件サイトで検索するときは駅から5分以内や10分以内でフィルターをかけて検索することが多いので、駅から10分以上かかるような最寄り駅と駅からギリギリ10分という最寄り駅があったら、後者を最寄り駅として書くのかもしれません。&lt;/p&gt;
&lt;details&gt;
&lt;summary&gt;Code&lt;/summary&gt;
&lt;pre&gt;&lt;code class="language-r"&gt;df4 |&amp;gt;
mutate(moyorieki_1_walk=if_else(moyorieki_1_walk &amp;gt;= 30L, 30L, moyorieki_1_walk)) |&amp;gt;
ggplot(aes(moyorieki_1_walk))+
theme_light()+
geom_histogram(binwidth=1)+
labs(x=&amp;quot;moyorieki_1_walk（1個目の最寄り駅からの徒歩分数）&amp;quot;, y=&amp;quot;物件数（部屋）&amp;quot;)+
theme(
strip.background=element_rect(color=&amp;quot;black&amp;quot;, fill=&amp;quot;white&amp;quot;),
strip.text=element_text(color=&amp;quot;black&amp;quot;)
)+
facet_wrap(~ward, ncol=4, scales=&amp;quot;free_y&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;/details&gt;
&lt;img src="images/image9-1.png" style="width:80.0%" /&gt;
&lt;h2 id="モデリング"&gt;モデリング&lt;/h2&gt;
&lt;h3 id="モデルの設計"&gt;モデルの設計&lt;/h3&gt;
&lt;p&gt;長かったですが、可視化の結果とドメイン知識を踏まるとこんな感じです。改めてですが、断りがない限り「家賃」は「家賃+管理費」を指します。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;横軸を面積、縦軸を家賃に取ると、最寄り駅によって切片と傾きが違う&lt;/li&gt;
&lt;li&gt;面積と間取りは家賃に与える情報が被りそう&lt;/li&gt;
&lt;li&gt;築年数や徒歩分数が増えるほど家賃は下がる&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;2つ目はちょっとだけ意外かもしれませんが、そりゃそうだよねという感じです。可視化の結果誰も知らないことが分かることはあまりなく、えてして皆が知っていることをデータから再確認するものなのです。&lt;/p&gt;
&lt;p&gt;同じ面積でも最寄り駅によって家賃は違いそうです。また、面積が2倍になればある最寄り駅の物件では家賃が1.5倍になるかもしれないし、ある最寄り駅では3倍になるかもしれないですね。&lt;/p&gt;
&lt;p&gt;築年数は1年増えるごとに、徒歩分数は1分増えるごとに定数が乗算されて家賃が下がるモデルにしてみます。つまり新築で10万円の物件なら築5年で9万円、築10年で8万円として、新築で20万円の物件なら築5年で18万円、築10年なら16万円というイメージでしょう。家賃の絶対水準が違うからですね。&lt;/p&gt;
&lt;p&gt;これは築年数や徒歩分数は家賃の対数に対して負の線形の関係があるということです。特に築年数に関して言うと、建物は一年あたりの減価率の年数乗で減価するというのは直感（ドメイン知識）と合っています。減価償却の定率法的な感じです。&lt;/p&gt;
&lt;p&gt;ここまで書いたことを以下のようにモデルで表現してみました。&lt;/p&gt;
&lt;p&gt;物件$i(1, \dots, N)$の最寄り駅（SUUMOの物件ページで一番上に書いてある1番目の最寄り駅）を$sta[i] (1, \dots, S)$とします。&lt;/p&gt;
&lt;p&gt;$$
\begin{align*}
\log{y_{i}} &amp;amp; \sim N(\mu_{i}, \sigma) \\\
\mu_{i} &amp;amp;= a_{sta[i]} + b_{sta[i]} \log{\mathrm{area}_{i}} \\\
&amp;amp;+ \beta_{\mathrm{age}} \mathrm{age}_{i} + \beta_{\mathrm{walk}}(\mathrm{walk}_{i} - 1) \\\
a_{sta[i]} &amp;amp; \sim N(a_{all}, \sigma_{a_{all}}) \\\
b_{sta[i]} &amp;amp; \sim N(b_{all}, \sigma_{b_{all}}) \\\
\end{align*}
$$&lt;/p&gt;
&lt;p&gt;ただし、物件$i$について、それぞれ以下の通りとします。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$y_{i}$: 家賃+管理費（万円）&lt;/li&gt;
&lt;li&gt;$\mathrm{area}_{i}$: 面積（m2）&lt;/li&gt;
&lt;li&gt;$\mathrm{age}_{i} (0 \leq \mathrm{age}_{i} \leq 40)$:
築年数（年、整数）。新築は0年とする&lt;/li&gt;
&lt;li&gt;$\mathrm{walk}_{i} (1 \leq \mathrm{walk}_{i} \leq 20)$:
最寄り駅からの徒歩分数（分、整数）。徒歩0分という物件はない&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;最寄り駅が$sta[i]$, 面積が$\mathrm{area}_{i}$（m2）,
築年数が$\mathrm{age}_{i}$（年）,
最寄り駅から徒歩$\mathrm{walk}_{i}$（分）の物件$i$について、その家賃（管理費込み）の対数$\log{y_{i}}$（万円）は、平均$\mu_{i}$,
標準偏差$\sigma$の正規分布に従うと仮定したモデルです。ただし、最寄り駅が複数あっても1個しか考慮しません。要するに物件の最寄り駅で階層化した面積のランダム切片＋ランダム係数モデルに、築年数と最寄り駅からの徒歩分数による減価要素を入れたものです。&lt;/p&gt;
&lt;p&gt;このとき、この最寄り駅、面積、築年数、徒歩分数での条件における物件の対数家賃の相場は$\mu_{i}$万円であると考えます。&lt;/p&gt;
&lt;p&gt;最寄り駅ごとに回帰直線を別々に推定するのではなく階層モデルにすると何がうれしいのかというと、サンプル数の少ない駅でも全体の傾向を借用してパラメータを推定することができます。これを縮約といいます。また、各最寄り駅のパラメータ$a_{sta[i]}, b_{sta[i]}$は、$a_{all}, b_{all}$という全ての最寄り駅の「平均的な」パラメータから$\sigma_{a_{all}}, \sigma_{b_{all}}$だけばらつくという、ドメイン知識に合ったメカニズムを組み込むことができるのもメリットです。&lt;/p&gt;
&lt;p&gt;なお、$y_{i}$と$area_{i}$は対数変換しない選択肢もありますが、いま定式化したように、&lt;code&gt;家賃 + 管理費 = (a + b * 面積) * 築年数効果 * 徒歩分数効果&lt;/code&gt;という右辺が乗算のモデルを考えるなら、左辺を対数変換すると右辺を加算の関係に変換することができるので、StanでのMCMCが収束しやすいというメリットもあります。&lt;code&gt;a*築年数効果&lt;/code&gt;のようなパラメータ同士の積があると収束しにくいです。&lt;/p&gt;
&lt;h3 id="モデルに投入するデータ"&gt;モデルに投入するデータ&lt;/h3&gt;
&lt;p&gt;投入するデータは、SUUMOに掲載されている東京23区のデータのうち、以下を除外した178113件のデータです。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;1.1個目の最寄り駅から徒歩ではない物件（バスや車） [1025件]&lt;/li&gt;
&lt;li&gt;2.賃貸マンション、賃貸アパート以外の物件（一戸建てなど）[5392件]&lt;/li&gt;
&lt;li&gt;3.物件の階数の情報がない物件 [11件]&lt;/li&gt;
&lt;li&gt;4.物件の階数が建物の地上階の階数より高い物件、または地下階の高さより低い物件
[189件]&lt;/li&gt;
&lt;li&gt;5.家賃+管理費が100万円を超える物件 [1286件]&lt;/li&gt;
&lt;li&gt;6.面積が100m2を超える物件 [436件]&lt;/li&gt;
&lt;li&gt;7.面積が10m2未満の物件 [1033件]&lt;/li&gt;
&lt;li&gt;8.築年数が40年を超える物件 [22565件]
&lt;ul&gt;
&lt;li&gt;40年超の物件は数が減るため&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;9.1個目の最寄り駅からの徒歩分数が20分を超える物件 [1852件]
&lt;ul&gt;
&lt;li&gt;8と同様&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;1～4は可視化の時点で既に除外していました。5～9は可視化の結果を踏まえてモデルの推定では追加で除外することにしました。&lt;/p&gt;
&lt;p&gt;徒歩分数の方はともかく築年数の方は全体の1割なので少なくはないですが、今回は築年数が40年まで見られれば自分の関心を満たせるということでいったん40年で切りました。可視化の章で述べた通り40年を目安に適用される建築基準法が異なる問題もあるので、家賃相場の傾向も変わってくる可能性があります。40年で分けて考えるのは悪くなさそうです。&lt;/p&gt;
&lt;h2 id="stanの実装"&gt;Stanの実装&lt;/h2&gt;
&lt;p&gt;以下のコードです。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-stan"&gt;data {
int N; // 物件の数
vector[N] Y; // 物件nの家賃+管理費
vector[N] AREA; // 物件nの面積
int S; // 最寄り駅の数
int&amp;lt;lower=1, upper=S&amp;gt; STATION[N]; // 物件nの最寄り駅index
vector[N] AGE; // 物件nの築年数（lower=0, upper=40の整数）
vector[N] WALK; // 物件nの徒歩分数（lower=1, upper=20の整数）
}
parameters {
real a0; // 面積の切片の全体平均
real b0; // 面積の傾きの全体平均
vector[S] a;
vector[S] b;
real&amp;lt;upper=0&amp;gt; age_b;
real&amp;lt;upper=0&amp;gt; walk_b;
real&amp;lt;lower=0&amp;gt; sigma_a;
real&amp;lt;lower=0&amp;gt; sigma_b;
real&amp;lt;lower=0&amp;gt; sigma; // 物件ごとのばらつき
}
model {
// 最寄り駅による面積の階層効果
a ~ normal(a0, sigma_a);
b ~ normal(b0, sigma_b);
log(Y) ~ normal(a[STATION] + b[STATION] .* log(AREA) + age_b*AGE + walk_b*(WALK - 1), sigma);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;事前分布は無情報事前分布です。&lt;/p&gt;
&lt;p&gt;全体平均の&lt;code&gt;a0&lt;/code&gt;と&lt;code&gt;b0&lt;/code&gt;を正規分布の平均として織り込むというのは階層モデルのMCMC推定の高速化と収束しやすくするテクニックです。アヒル本（&lt;a href="https://www.amazon.co.jp/dp/4320112423" target="_blank" rel="noopener noreferrer"&gt;StanとRでベイズ統計モデリング&lt;/a&gt;）の8.1.6章を参考にしました。&lt;/p&gt;
&lt;p&gt;高速化のためにベクトル化しているので少し分かりづらいですが、ベクトル化しないと以下のコードです。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-stan"&gt;model {
for (s in 1:S) {
a[s] ~ normal(a0, sigma_a);
b[s] ~ normal(b0, sigma_b);
}
for (n in 1:N) {
log(Y[n]) ~ normal(a[STATION[n]] + b[STATION[n]]*log(AREA[n]) + age_b*AGE[n] + walk_b*(WALK[n] - 1), sigma);
}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;このコードを”model.stan”で保存して以下のRコードでキックします。chains=4,
iter=5000, warmup=1000で約12時間かかりました。&lt;/p&gt;
&lt;details&gt;
&lt;summary&gt;Code&lt;/summary&gt;
&lt;pre&gt;&lt;code class="language-r"&gt;# 特徴量生成
df_mod &amp;lt;- df4 |&amp;gt;
filter(rent_admin &amp;lt;= 50 &amp;amp; area &amp;lt;= 100) |&amp;gt;
filter(moyorieki_1_walk &amp;lt;= 20 &amp;amp; age &amp;lt;= 40) |&amp;gt;
# Stanに渡すために最寄り駅 (character) をfactorを経由してintegerにする
mutate(moyorieki_1_station_index=as.integer(as.factor(moyorieki_1_station)))
&lt;/code&gt;&lt;/pre&gt;
&lt;/details&gt;
&lt;pre&gt;&lt;code class="language-r"&gt;# 上はMCMCの並列化、下はstanコードが変わらない限り再コンパイルしない
options(mc.cores=parallel::detectCores())
rstan::rstan_options(auto_write=TRUE)
# Stanコードのコンパイル
mod &amp;lt;- rstan::stan_model(&amp;quot;model.stan&amp;quot;)
# MCMCの実行
fit &amp;lt;- rstan::sampling(
mod,
data=list(
N=nrow(df_mod),
Y=df_mod$rent_admin,
AREA=df_mod$area,
S=length(unique(df_mod$moyorieki_1_station_index)),
STATION=df_mod$moyorieki_1_station_index,
AGE=df_mod$age,
WALK=df_mod$moyorieki_1_walk
),
chains=4, iter=5000, warmup=1000, thin=1, refresh=10, seed=1234
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;デフォルトでは&lt;code&gt;iter/10&lt;/code&gt;ごとにprogressが出力されますが、MCMCの推定に時間がかかるようなモデルの場合は中々出力されず不安になるので&lt;code&gt;refresh&lt;/code&gt;に小さい値を指定しておくことで細かくprogressを表示しておくといいです。&lt;/p&gt;
&lt;h2 id="推定結果のチェック"&gt;推定結果のチェック&lt;/h2&gt;
&lt;h3 id="mcmcのチェック"&gt;MCMCのチェック&lt;/h3&gt;
&lt;p&gt;StanのMCMCが終わったら、パラメータが正しく推定されていることを確かめるために、以下をチェックします。bayesplotを使うと簡単にプロットできます。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;トレースプロットが混ざり合っていること
&lt;ul&gt;
&lt;li&gt;MCMCで生成されたパラメータのサンプルが初期値によらず同じ値に収束しているか（＝局所解に落ちていないか）の確認&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Rhat &amp;lt; 1.1であること
&lt;ul&gt;
&lt;li&gt;上の収束の度合いを数値で表したもの&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;有効サンプルサイズn_effが大きいこと
&lt;ul&gt;
&lt;li&gt;サンプルが互いに独立であることを示す（＝定常な分布に収束している）&lt;/li&gt;
&lt;li&gt;目安はサンプル数で割った値が0.1以上&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;パラメータのサンプルに自己相関がないこと
&lt;ul&gt;
&lt;li&gt;上を自己相関係数のプロットで示したもの&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;事後診断の詳細はこちらの素晴らしい記事も参考にさせていただきました。&lt;/p&gt;
&lt;p&gt;&lt;a href="https://ill-identified.hatenablog.com/entry/2019/06/13/010510" target="_blank" rel="noopener noreferrer"&gt;[R] [stan] bayesplot を使ったモンテカルロ法の実践ガイド -
ill-identified
diary&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;以下にプロットを載せます。きれいなプロットで全てよさそうです。&lt;/p&gt;
&lt;p&gt;スペース的に全部のパラメータを載せることは難しいのでトレースプロット（1枚目）と自己相関（4枚目）で描くパラメータは一部のパラメータに絞っていますが、他のパラメータも問題ありませんでした。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-r"&gt;# トレースプロット
bayesplot::mcmc_trace(fit, pars=c(&amp;quot;a0&amp;quot;, &amp;quot;b0&amp;quot;, &amp;quot;age_b&amp;quot;, &amp;quot;walk_b&amp;quot;))
&lt;/code&gt;&lt;/pre&gt;
&lt;img src="images/image10-1.png" style="width:80.0%" /&gt;
&lt;pre&gt;&lt;code class="language-r"&gt;# Rhatのヒストグラム
bayesplot::mcmc_rhat_hist(bayesplot::rhat(fit))
&lt;/code&gt;&lt;/pre&gt;
&lt;img src="images/image10-2.png" style="width:80.0%" /&gt;
&lt;pre&gt;&lt;code class="language-r"&gt;# n_eff/Nのヒストグラム
bayesplot::mcmc_neff_hist(bayesplot::neff_ratio(fit))
&lt;/code&gt;&lt;/pre&gt;
&lt;img src="images/image10-3.png" style="width:80.0%" /&gt;
&lt;pre&gt;&lt;code class="language-r"&gt;# 自己相関係数のプロット
bayesplot::mcmc_acf_bar(fit, pars=c(&amp;quot;a0&amp;quot;, &amp;quot;b0&amp;quot;, &amp;quot;age_b&amp;quot;, &amp;quot;walk_b&amp;quot;))
&lt;/code&gt;&lt;/pre&gt;
&lt;img src="images/image10-4.png" style="width:80.0%" /&gt;
&lt;h3 id="モデルの当てはまりのチェック"&gt;モデルの当てはまりのチェック&lt;/h3&gt;
&lt;p&gt;MCMCのサンプリングがうまく収束したことは分かりましたが、そもそも今回設定したモデルは現実の家賃データに当てはまっているのか？ということを確かめます。&lt;/p&gt;
&lt;p&gt;これは、Stanの&lt;code&gt;generated quantitiles&lt;/code&gt;ブロックに以下のように書いてStan上で予測値を生成すればいいです。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-stan"&gt;generated quantities {
vector[N] y_pred;
for (n in 1:N) {
y_pred[n] ~ exp(normal_rng(a[STATION[n]] + b[STATION[n]] * log(AREA[n]) + age_b*AGE[n] + walk_b*(WALK[n] - 1), sigma));
}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;しかし、N=18万弱の&lt;code&gt;y_pred&lt;/code&gt;それぞれについて、&lt;code&gt;(5000 (iter) - 1000 (warmup)) / 1 (thin) * 4 (chain) = 16000&lt;/code&gt;個（draw）の予測値をStanで生成するため、&lt;code&gt;rstan::extract(fit)$y_pred&lt;/code&gt;は16000
x
18万弱のmatrixとなります。これでは&lt;code&gt;rstan::sampling()&lt;/code&gt;の返り値はとんでもなく大きいサイズのオブジェクトになってしまいます。&lt;/p&gt;
&lt;p&gt;大まかな事後予測チェックには予測値はそんなにたくさんなくてもいいと思い、Stanで上のコードを書くのではなく、Rでパラメータの事後分布からサンプリングして予測値を100個生成することにしました。要するに100
x
18万弱のmatrixを作る（このサイズのmatrixでも150MBあります）ということです。（Stanコードで&lt;code&gt;generated quantities&lt;/code&gt;の予測値をdraw個ではなく100個など任意の個数に絞る方法はあるのでしょうか…）&lt;/p&gt;
&lt;p&gt;furrrで並列化して10秒くらいで生成できました。&lt;/p&gt;
&lt;details&gt;
&lt;summary&gt;Code&lt;/summary&gt;
&lt;pre&gt;&lt;code class="language-r"&gt;# MCMCのサンプルを取り出す
# matrix (draws x stations)
sample_a &amp;lt;- rstan::extract(fit, &amp;quot;a&amp;quot;)$a
sample_b &amp;lt;- rstan::extract(fit, &amp;quot;b&amp;quot;)$b
# vector (length = draws)
sample_age_b &amp;lt;- rstan::extract(fit, &amp;quot;age_b&amp;quot;)$age_b
sample_walk_b &amp;lt;- rstan::extract(fit, &amp;quot;walk_b&amp;quot;)$walk_b
sample_sigma_a &amp;lt;- rstan::extract(fit, &amp;quot;sigma_a&amp;quot;)$sigma_a
sample_sigma_b &amp;lt;- rstan::extract(fit, &amp;quot;sigma_b&amp;quot;)$sigma_b
sample_sigma &amp;lt;- rstan::extract(fit, &amp;quot;sigma&amp;quot;)$sigma
# 予測値の中央値を求める関数（n_pred個の予測値を作る）
calc_pred &amp;lt;- function(station_idx, area, age, walk, n_pred, seed) {
mu &amp;lt;- sample_a[,station_idx] + sample_b[,station_idx]*log(area) + sample_age_b*age + sample_walk_b*(walk - 1)
mu2 &amp;lt;- withr::with_seed(seed, sample(mu, size=n_pred))
sigma2 &amp;lt;- withr::with_seed(seed, sample(sample_sigma, size=n_pred))
map2_dbl(mu2, sigma2, function(x, y) {
withr::with_seed(seed, rnorm(1, mean=x, sd=y))
})
}
station_idxs &amp;lt;- df_mod$moyorieki_1_station_index
areas &amp;lt;- df_mod$area
ages &amp;lt;- df_mod$age
walks &amp;lt;- df_mod$moyorieki_1_walk
rent_admins &amp;lt;- df_mod$rent_admin
variables &amp;lt;- list(
station_idx=station_idxs,
area=areas,
age=ages,
walk=walks
)
# 予測値を求める
future::plan(future::multisession)
y_preds &amp;lt;- furrr::future_pmap(
variables,
function(station_idx, area, age, walk) {
calc_pred(station_idx, area, age, walk, n_pred=100, seed=1234)
},
.progress=TRUE,
.options=furrr::furrr_options(seed=1234)
)
# bayesplotに渡すためにsample (n_pred) x data length (180000)のmatrixに変換する
y_pred &amp;lt;- simplify2array(y_preds)
&lt;/code&gt;&lt;/pre&gt;
&lt;/details&gt;
&lt;p&gt;以下は調整済みではない決定係数です。1個目の結果は家賃の対数との比較、2個目は対数ではない家賃との比較です。trainとtestのsplitはしていないので、学習データ内での決定係数です。予測値を100個しか作っていないので参考程度ですが、0.91程度なので悪くないでしょう。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-r"&gt;MLmetrics::R2_Score(apply(y_pred, 2, median), log(rent_admins))
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;#&amp;gt; [1] 0.9294308
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class="language-r"&gt;MLmetrics::R2_Score(apply(exp(y_pred), 2, median), rent_admins)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;#&amp;gt; [1] 0.9082848
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;次は予測値（横軸）と実際の値（縦軸）のプロットです。左のプロットは対数家賃、右は対数を外した家賃です。&lt;/p&gt;
&lt;details&gt;
&lt;summary&gt;Code&lt;/summary&gt;
&lt;pre&gt;&lt;code class="language-r"&gt;patchwork::wrap_plots(
bayesplot::ppc_scatter_avg(log(rent_admins), y_pred, size=0.1)+
geom_abline(slope=1, intercept=0)+
labs(title=&amp;quot;対数家賃&amp;quot;),
bayesplot::ppc_scatter_avg(rent_admins, exp(y_pred), size=0.1)+
geom_abline(slope=1, intercept=0)+
labs(title=&amp;quot;家賃&amp;quot;),
ncol=2
)
&lt;/code&gt;&lt;/pre&gt;
&lt;/details&gt;
&lt;img src="images/image11-1.png" style="width:80.0%" /&gt;
&lt;p&gt;左の対数家賃が45度線上に載っています。&lt;/p&gt;
&lt;p&gt;次は残差のプロットです。横軸は面積、縦軸は残差（予測値-実際の値）です。&lt;/p&gt;
&lt;details&gt;
&lt;summary&gt;Code&lt;/summary&gt;
&lt;pre&gt;&lt;code class="language-r"&gt;patchwork::wrap_plots(
bayesplot::ppc_error_scatter_avg_vs_x(log(rent_admins), y_pred, df_mod$area, size=0.1)+
labs(title=&amp;quot;対数家賃&amp;quot;)+
geom_vline(xintercept=0)+
scale_y_continuous(breaks=seq(0, 100, 20)),
bayesplot::ppc_error_scatter_avg_vs_x(rent_admins, exp(y_pred), df_mod$area, size=0.1)+
labs(title=&amp;quot;家賃&amp;quot;)+
geom_vline(xintercept=0)+
scale_y_continuous(breaks=seq(0, 100, 20)),
ncol=2
)
&lt;/code&gt;&lt;/pre&gt;
&lt;/details&gt;
&lt;img src="images/image12-1.png" style="width:80.0%" /&gt;
&lt;p&gt;左の図を見ると、y=0の直線の上下にだいたい均等に分布しているので、回帰モデルの前提である残差の等分散性と正規性がだいたい満たされていそうです。おおむね悪くなさそうです。&lt;/p&gt;
&lt;p&gt;ただ、x &amp;lt;=
15m2くらいの面積が小さいゾーンで縦軸が正の点が多いことが分かります。つまり15m2を切るような物件では過少に予測しがちということですね。10m2未満の物件を除外しましたが、除外する閾値を15m2に上げてもよかったかもしれません。&lt;/p&gt;
&lt;p&gt;また、70m2～以上の物件も同様に過少に予測しがちでした。この面積帯はファミリー向けなので、一人暮らし～二人暮らしゾーンの20～60m2程度の物件とはちょっとメカニズムが違うのかもしれません。&lt;/p&gt;
&lt;h2 id="結果"&gt;結果&lt;/h2&gt;
&lt;p&gt;ようやく結果を見るところまでたどり着きました。こちらが推定されたパラメータの結果です。ただしprintが長くなるので&lt;code&gt;a[s]&lt;/code&gt;,
&lt;code&gt;b[s]&lt;/code&gt;は省略します。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-r"&gt;print(fit, pars=c(&amp;quot;a0&amp;quot;, &amp;quot;b0&amp;quot;, &amp;quot;age_b&amp;quot;, &amp;quot;walk_b&amp;quot;, &amp;quot;sigma_a&amp;quot;, &amp;quot;sigma_b&amp;quot;, &amp;quot;sigma&amp;quot;), digits_summary=3)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;#&amp;gt; Inference for Stan model: anon_model.
#&amp;gt; 4 chains, each with iter=5000; warmup=1000; thin=1;
#&amp;gt; post-warmup draws per chain=4000, total post-warmup draws=16000.
#&amp;gt;
#&amp;gt; mean se_mean sd 2.5% 25% 50% 75% 97.5% n_eff Rhat
#&amp;gt; a0 -0.101 0 0.015 -0.131 -0.111 -0.101 -0.091 -0.071 25141 1
#&amp;gt; b0 0.804 0 0.006 0.792 0.800 0.804 0.808 0.817 22027 1
#&amp;gt; age_b -0.011 0 0.000 -0.011 -0.011 -0.011 -0.010 -0.010 17203 1
#&amp;gt; walk_b -0.009 0 0.000 -0.009 -0.009 -0.009 -0.009 -0.009 28102 1
#&amp;gt; sigma_a 0.313 0 0.011 0.292 0.306 0.313 0.321 0.337 20686 1
#&amp;gt; sigma_b 0.137 0 0.005 0.128 0.133 0.136 0.140 0.146 26200 1
#&amp;gt; sigma 0.125 0 0.000 0.125 0.125 0.125 0.125 0.126 15352 1
#&amp;gt;
#&amp;gt; Samples were drawn using NUTS(diag_e) at Sun Dec 24 15:16:08 2023.
#&amp;gt; For each parameter, n_eff is a crude measure of effective sample size,
#&amp;gt; and Rhat is the potential scale reduction factor on split chains (at
#&amp;gt; convergence, Rhat=1).
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id="築年数効果"&gt;築年数効果&lt;/h3&gt;
&lt;p&gt;以下、点推定値としてmedianを採用します&lt;sup id="fnref:4"&gt;&lt;a href="#fn:4" class="footnote-ref" role="doc-noteref"&gt;4&lt;/a&gt;&lt;/sup&gt;。$\beta_{\mathrm{age}}$ =
-0.011でした。これは、築年数が1年増えるごとに、家賃+管理費の対数が0.011小さくなることを意味します。&lt;/p&gt;
&lt;p&gt;と言われてもよく分からないですね。また、築年数が$m$年の物件は新築と比べてどの程度家賃が下がるのかも知りたいです。&lt;/p&gt;
&lt;p&gt;この疑問に答えるには、$\mathrm{age}_{i} = 1, \dots, 40$としたときの$\exp (\beta_{\mathrm{age}} \mathrm{age}_{i})$の事後中央値と95%ベイズ信用区間を求めればよいです。&lt;code&gt;tidybayes::spread_draws()&lt;/code&gt;を使うと&lt;code&gt;rstan::sampling()&lt;/code&gt;の返り値からサンプルをtidyな形で取り出すことができて計算が楽です。&lt;/p&gt;
&lt;details&gt;
&lt;summary&gt;Code&lt;/summary&gt;
&lt;pre&gt;&lt;code class="language-r"&gt;tidy_draws &amp;lt;- tidybayes::spread_draws(fit, age_b, walk_b, sigma_a, sigma_b, sigma)
age_b &amp;lt;- tidy_draws |&amp;gt;
pull(age_b)
# 1年 - 40年
res_age &amp;lt;- 1:40 |&amp;gt;
map_dfr(\(age) {
samples &amp;lt;- exp(age_b * age)
tibble::tibble(
age=age,
median=quantile(samples, 0.5),
lower=quantile(samples, 0.975),
upper=quantile(samples, 0.025)
)
})
# きりのいいageだけ表示する
res_age |&amp;gt;
filter(age %in% c(0:5, seq(5, 40, 5))) |&amp;gt;
print(n=15)
&lt;/code&gt;&lt;/pre&gt;
&lt;/details&gt;
&lt;pre&gt;&lt;code&gt;#&amp;gt; # A tibble: 12 × 4
#&amp;gt; age median lower upper
#&amp;gt; &amp;lt;int&amp;gt; &amp;lt;dbl&amp;gt; &amp;lt;dbl&amp;gt; &amp;lt;dbl&amp;gt;
#&amp;gt; 1 1 0.990 0.990 0.990
#&amp;gt; 2 2 0.979 0.979 0.979
#&amp;gt; 3 3 0.969 0.969 0.969
#&amp;gt; 4 4 0.959 0.959 0.959
#&amp;gt; 5 5 0.949 0.949 0.949
#&amp;gt; 6 10 0.900 0.901 0.900
#&amp;gt; 7 15 0.854 0.855 0.854
#&amp;gt; 8 20 0.810 0.811 0.810
#&amp;gt; 9 25 0.769 0.770 0.768
#&amp;gt; 10 30 0.730 0.731 0.729
#&amp;gt; 11 35 0.692 0.693 0.691
#&amp;gt; 12 40 0.657 0.658 0.656
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;medianは事後分布の中央値、upperとlowerは95%CIの上限と下限です。&lt;/p&gt;
&lt;p&gt;medianの列の通り、築年数が1年増えるごとに家賃は0.99倍になります。新築の物件と比較すると、築5年の物件で5%、10年で10%、15年で15%、20年で19%家賃が下がります。築25年くらいまではほぼ1年で1%減ると近似できて覚えやすいです&lt;sup id="fnref:5"&gt;&lt;a href="#fn:5" class="footnote-ref" role="doc-noteref"&gt;5&lt;/a&gt;&lt;/sup&gt;。これは参考にできそうな知見ですね。個人的に参考にしようと思いました。&lt;/p&gt;
&lt;p&gt;物件数が18万件と多いおかげでサンプルの標準誤差が小さいためにmedianもupperもlowerもほぼ一致しています。データ数は正義。&lt;/p&gt;
&lt;p&gt;次の徒歩分数や最寄り駅別の家賃相場の結果も同様ですが、「築年数が1年増えるごとに家賃は0.99倍」というのは今回設定したモデルでの数値であって、モデルの設定が変われば数値も変わることに注意してください。&lt;/p&gt;
&lt;h3 id="最寄り駅からの徒歩分数効果"&gt;最寄り駅からの徒歩分数効果&lt;/h3&gt;
&lt;p&gt;最寄り駅からの徒歩分数が伸びるとどの程度家賃が下がるのでしょうか？築年数効果と同じように、$\mathrm{walk}_{i} = 2, \dots, 20$としたときの$\exp (\beta_{\mathrm{walk}} (\mathrm{walk}_{i} - 1))$の事後中央値と95%ベイズ信用区間を求めれば分かります。&lt;/p&gt;
&lt;details&gt;
&lt;summary&gt;Code&lt;/summary&gt;
&lt;pre&gt;&lt;code class="language-r"&gt;walk_b &amp;lt;- fit |&amp;gt;
tidybayes::spread_draws(walk_b) |&amp;gt;
pull(walk_b)
# 2分 - 20分
res_walk &amp;lt;- 2:20 |&amp;gt;
map_dfr(\(walk) {
samples &amp;lt;- exp(walk_b * (walk - 1))
tibble::tibble(
walk=walk,
median=quantile(samples, 0.5),
lower=quantile(samples, 0.975),
upper=quantile(samples, 0.025)
)
})
# きりのいいwalkだけ表示する
res_walk |&amp;gt;
filter(walk %in% c(2, 3, 5, 10, 15, 20)) |&amp;gt;
print()
&lt;/code&gt;&lt;/pre&gt;
&lt;/details&gt;
&lt;pre&gt;&lt;code&gt;#&amp;gt; # A tibble: 6 × 4
#&amp;gt; walk median lower upper
#&amp;gt; &amp;lt;int&amp;gt; &amp;lt;dbl&amp;gt; &amp;lt;dbl&amp;gt; &amp;lt;dbl&amp;gt;
#&amp;gt; 1 2 0.991 0.991 0.991
#&amp;gt; 2 3 0.982 0.982 0.982
#&amp;gt; 3 5 0.964 0.965 0.964
#&amp;gt; 4 10 0.921 0.923 0.920
#&amp;gt; 5 15 0.880 0.882 0.878
#&amp;gt; 6 20 0.841 0.844 0.838
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;駅からの徒歩分数が1分増えるごとに家賃は0.991倍になります。駅から徒歩1分の物件と比較すると、家賃は徒歩5分の物件で4%、10分で8%、15分で12%、20分で16%下がるようです。&lt;/p&gt;
&lt;p&gt;ただし、新築と築数年や、徒歩1分と徒歩5分などはもう少しだけ離れていても不思議ではないかなという気もします。&lt;/p&gt;
&lt;h3 id="最寄り駅別の家賃相場面積固定"&gt;最寄り駅別の家賃相場（面積固定）&lt;/h3&gt;
&lt;p&gt;最寄り駅によってどの程度家賃が変わるか見てみましょう。同一の路線内で見るとイメージが付いて面白いです。&lt;/p&gt;
&lt;p&gt;実際のところ、新築で徒歩1分の物件はあまりないので相場感をイメージしづらいですね。現実的なところで築5年、徒歩5分で30m2の物件の相場を見てみましょう。これは$\mathrm{area}_{i} = 30, \mathrm{age}_{i} = 5, \mathrm{walk}_{i} = 5$としたときの$\exp(\mu_{i})$の事後中央値と95%ベイズ信用区間を計算します。&lt;/p&gt;
&lt;p&gt;京王線・京王新線です。新宿から西の方に伸びる路線ですね。北の方にJR中央線、南の方に小田急線が並行して走っています。&lt;/p&gt;
&lt;details&gt;
&lt;summary&gt;Code&lt;/summary&gt;
&lt;pre&gt;&lt;code class="language-r"&gt;# 駅名とモデルに投入したindexのマッピング
sta_chr_idx_table &amp;lt;- df_mod |&amp;gt;
select(moyorieki_1_station, moyorieki_1_station_index) |&amp;gt;
distinct(moyorieki_1_station, .keep_all=TRUE)
# 駅名があればそのindex, なければNA_integer_を返す
station_to_idx &amp;lt;- function(station_name) {
chr &amp;lt;- sta_chr_idx_table$moyorieki_1_station
idx &amp;lt;- sta_chr_idx_table$moyorieki_1_station_index
if (length(idx[which(chr==station_name)]) == 0) {
return(NA_integer_)
} else {
return(idx[which(chr==station_name)])
}
}
tidy_draws_by_idx &amp;lt;- tidybayes::spread_draws(fit, a[idx], b[idx], age_b, walk_b, sigma_a, sigma_b)
stations &amp;lt;- c(
&amp;quot;新宿駅&amp;quot;, &amp;quot;初台駅&amp;quot;, &amp;quot;幡ヶ谷駅&amp;quot;, &amp;quot;笹塚駅&amp;quot;, &amp;quot;代田橋駅&amp;quot;, &amp;quot;明大前駅&amp;quot;, &amp;quot;下高井戸駅&amp;quot;, &amp;quot;桜上水駅&amp;quot;, &amp;quot;上北沢駅&amp;quot;, &amp;quot;八幡山駅&amp;quot;, &amp;quot;芦花公園駅&amp;quot;, &amp;quot;千歳烏山駅&amp;quot;
)
# factor型で駅の路線順に並べる
stations_fct &amp;lt;- forcats::fct_relevel(as.factor(stations), stations)
# 見る駅名のindex（stanのa[s]やb[s]のs）
idxs &amp;lt;- map_int(stations, station_to_idx)
area &amp;lt;- 30
age &amp;lt;- 5
walk &amp;lt;- 5
p1 &amp;lt;- tidy_draws_by_idx |&amp;gt;
filter(idx %in% idxs) |&amp;gt;
# 駅のindexではなく駅名をプロットに付けるためにindexと駅名のテーブルをjoinする
left_join(
df_mod |&amp;gt;
filter(moyorieki_1_station %in% stations) |&amp;gt;
distinct(moyorieki_1_station, .keep_all=TRUE) |&amp;gt;
select(moyorieki_1_station, moyorieki_1_station_index) |&amp;gt;
rename(station=moyorieki_1_station, idx=moyorieki_1_station_index) |&amp;gt;
mutate(station=forcats::fct_relevel(station, stations)),
by=&amp;quot;idx&amp;quot;
) |&amp;gt;
mutate(mu_exp=exp(a+b*log(area)+age_b*age+walk_b*(walk-1))) |&amp;gt;
ggplot(aes(mu_exp, station))+
theme_light()+
tidybayes::stat_pointinterval(point_interval=tidybayes::median_qi, .width=0.95)+
scale_x_continuous(breaks=0:20)+
theme(axis.title.y=element_blank())+
labs(
title=&amp;quot;exp(mu_i) (age_i=5, walk_i=5, area_i=30)&amp;quot;,
subtitle=&amp;quot;point: estimated (median), bar: 95% bayesian CI&amp;quot;,
x=&amp;quot;exp(mu_i)&amp;quot;,
y=&amp;quot;station&amp;quot;
)
p2 &amp;lt;- df_mod |&amp;gt;
filter(moyorieki_1_station %in% stations) |&amp;gt;
count(moyorieki_1_station, moyorieki_1_station_index, name=&amp;quot;n&amp;quot;) |&amp;gt;
rename(station=moyorieki_1_station, idx=moyorieki_1_station_index) |&amp;gt;
mutate(station=forcats::fct_relevel(station, stations)) |&amp;gt;
arrange(station) |&amp;gt;
ggplot(aes(station, n))+
theme_light()+
geom_bar(stat=&amp;quot;identity&amp;quot;, color=&amp;quot;black&amp;quot;, fill=&amp;quot;gray&amp;quot;, alpha=0.6)+
scale_y_continuous(breaks=seq(0, 2000, 500), minor_breaks=seq(0, 2000, 100))+
geom_text(aes(label=n, y=100))+
theme(axis.title.y=element_blank())+
coord_flip()+
labs(
title=&amp;quot;（参考）物件数&amp;quot;,
subtitle=&amp;quot;築40年以下, 徒歩20分以下, 10m2-100m2の物件のみ&amp;quot;
)
patchwork::wrap_plots(p1, p2, ncol=2, widths=c(3, 2))
&lt;/code&gt;&lt;/pre&gt;
&lt;/details&gt;
&lt;img src="images/image13-1.png" style="width:80.0%" /&gt;
&lt;p&gt;y軸は路線図の順番通りに駅を並べています。物件数も合わせて示しているのは、そもそもそのエリアに物件がどのくらいあるのかという参考です。なお、家賃相場のモデルには路線名を入れていないので、左のプロットの推定家賃相場は京王線の新宿もJRなどの他の路線の新宿も同じになります。また、最寄り駅が新宿駅という物件数には、京王線以外の新宿駅が最寄り駅の物件も含まれています。&lt;/p&gt;
&lt;p&gt;左の図の点は事後中央値、点の左右の棒は95%ベイズ信用区間です。今回設定したモデルの下では、新宿から徒歩5分、築5年の30m2の物件の相場は17.4万円（95%CI:
17.1万円-17.8万円）ということです。&lt;/p&gt;
&lt;p&gt;今回設定したモデルの下では「新宿から徒歩5分の築5年の30m2の物件の家賃の平均的な姿」というパラメータがあり、これに「新宿から徒歩5分の築5年の30m2の物件の家賃相場」と名付けるなら、「家賃相場」の確率分布の95%は17.1万円-17.8万円の間に入るということを意味します。繰り返しですがモデル式が変われば図で示した家賃相場の数値は当然変わります。新宿から徒歩5分、築5年、30m2の実際の物件を集めて中央値を取ったら17.4万円でしたということでもありません。&lt;/p&gt;
&lt;p&gt;また、実際に観測される物件の家賃は、家賃相場にさらに$\sigma$というノイズが乗ります&lt;sup id="fnref:6"&gt;&lt;a href="#fn:6" class="footnote-ref" role="doc-noteref"&gt;6&lt;/a&gt;&lt;/sup&gt;。モデルに入れていない特徴量による物件の特徴での加算・減算分や、その他の説明のつかなかった色々なものを示します。なので、新宿から徒歩5分の築5年の30m2の物件の実際の家賃は、その95%が17.1万円-17.8万円の間に入るということではなく、17.1万円より小さいものから17.8万円より大きいものまでもっとあります。信用区間と予測区間の違いですね。&lt;/p&gt;
&lt;p&gt;この図に限らず知っている駅や路線をいくつか見てみると、だいたいの金額や駅間の相対的な水準はそんなに外してはいないかなと思いました。&lt;/p&gt;
&lt;p&gt;図に戻りますが、直感通り、新宿から遠ざかるほど相場が下がっていきます。明大前と代田橋は明大前の方が新宿から遠いですが、明大前の方が少し高いのは特急～各駅の全ての列車が止まること、井の頭線も通っているので渋谷や吉祥寺に出やすいからでしょうか。同様に千歳烏山は芦花公園より高いですが、千歳烏山も全ての列車が止まることが理由でしょうか。ただし95%ベイズ信用区間が広くて被っているので明確に差があるとは言いづらいです。&lt;/p&gt;
&lt;p&gt;千歳烏山の次の駅は仙川です。千歳烏山と仙川の間の東側が世田谷区、西側が調布市です。23区の物件をスクレイピングした都合上、最寄り駅が仙川という物件のうち、世田谷区にある物件しかデータに存在しません。これが仙川の推定値にどの程度影響を与えるか分からないので、仙川の相場は表には載せませんでした。&lt;/p&gt;
&lt;p&gt;わたし的には幡ヶ谷～桜上水がよさそうで気になりました。13.5万円前後の幡ヶ谷と笹塚は新宿まで5分、12.5万円の明大前は新宿と渋谷に10分で出られます。12万円弱の桜上水は駅の数としては新宿から離れますが急行が止まるので新宿まで15分です。閑静な住宅街という街ですね。安いかと言われるとどこも安くはないですが、新宿へのアクセスの良さを考えるとなかなかいいような気がします。&lt;/p&gt;
&lt;p&gt;ちなみに上の図はpatchworkというggplot2オブジェクトを上下左右に並べられるパッケージを使っています。このように左のプロットと右のプロットを3:2の幅で並べるという並べ方も簡単にできます。重宝するパッケージです。&lt;/p&gt;
&lt;p&gt;京王線・京王新線は新宿から遠ざかるほど家賃が安くなり、速達列車が止まる駅はすこしお高くなりました。都心と郊外を結ぶ路線はおおむねこのパターンです。一方で、都内の2駅を結ぶ路線はちょっと変わります。&lt;/p&gt;
&lt;p&gt;次の図は東急池上線（五反田～蒲田）です。池上線は各駅停車のみの路線です。&lt;/p&gt;
&lt;details&gt;
&lt;summary&gt;Code&lt;/summary&gt;
&lt;pre&gt;&lt;code class="language-r"&gt;stations &amp;lt;- c(
&amp;quot;五反田駅&amp;quot;, &amp;quot;大崎広小路駅&amp;quot;, &amp;quot;戸越銀座駅&amp;quot;, &amp;quot;荏原中延駅&amp;quot;, &amp;quot;旗の台駅&amp;quot;, &amp;quot;長原駅&amp;quot;, &amp;quot;洗足池駅&amp;quot;, &amp;quot;石川台駅&amp;quot;, &amp;quot;雪が谷大塚駅&amp;quot;, &amp;quot;御嶽山駅&amp;quot;, &amp;quot;久が原駅&amp;quot;, &amp;quot;千鳥町駅&amp;quot;, &amp;quot;池上駅&amp;quot;, &amp;quot;蓮沼駅&amp;quot;, &amp;quot;蒲田駅&amp;quot;
)
stations_fct &amp;lt;- forcats::fct_relevel(as.factor(stations), stations)
idxs &amp;lt;- map_int(stations, station_to_idx)
area &amp;lt;- 30
age &amp;lt;- 5
walk &amp;lt;- 5
p1 &amp;lt;- tidy_draws_by_idx |&amp;gt;
filter(idx %in% idxs) |&amp;gt;
left_join(
df_mod |&amp;gt;
filter(moyorieki_1_station %in% stations) |&amp;gt;
distinct(moyorieki_1_station, .keep_all=TRUE) |&amp;gt;
select(moyorieki_1_station, moyorieki_1_station_index) |&amp;gt;
rename(station=moyorieki_1_station, idx=moyorieki_1_station_index) |&amp;gt;
mutate(station=forcats::fct_relevel(station, stations)),
by=&amp;quot;idx&amp;quot;
) |&amp;gt;
mutate(mu_exp=exp(a+b*log(area)+age_b*age+walk_b*(walk-1))) |&amp;gt;
ggplot(aes(mu_exp, station))+
theme_light()+
tidybayes::stat_pointinterval(point_interval=tidybayes::median_qi, .width=c(0.95))+
scale_x_continuous(breaks=0:20)+
theme(axis.title.y=element_blank())+
labs(
title=&amp;quot;exp(mu_i) (age_i=5, walk_i=5, area_i=30)&amp;quot;,
subtitle=&amp;quot;point: estimated (median), bar: 95% bayesian CI&amp;quot;,
x=&amp;quot;exp(mu_i)&amp;quot;,
y=&amp;quot;station&amp;quot;
)
p2 &amp;lt;- df_mod |&amp;gt;
filter(moyorieki_1_station %in% stations) |&amp;gt;
count(moyorieki_1_station, moyorieki_1_station_index, name=&amp;quot;n&amp;quot;) |&amp;gt;
rename(station=moyorieki_1_station, idx=moyorieki_1_station_index) |&amp;gt;
mutate(station=forcats::fct_relevel(station, stations)) |&amp;gt;
arrange(station) |&amp;gt;
ggplot(aes(station, n))+
theme_light()+
geom_bar(stat=&amp;quot;identity&amp;quot;, color=&amp;quot;black&amp;quot;, fill=&amp;quot;gray&amp;quot;, alpha=0.6)+
scale_y_continuous(breaks=seq(0, 2000, 500), minor_breaks=seq(0, 2000, 100))+
geom_text(aes(label=n, y=100))+
theme(axis.title.y=element_blank())+
coord_flip()+
labs(
title=&amp;quot;（参考）物件数&amp;quot;,
subtitle=&amp;quot;築40年以下, 徒歩20分以下, 10m2-100m2の物件のみ&amp;quot;
)
patchwork::wrap_plots(p1, p2, ncol=2, widths=c(3, 2))
&lt;/code&gt;&lt;/pre&gt;
&lt;/details&gt;
&lt;img src="images/image14-1.png" style="width:80.0%" /&gt;
&lt;p&gt;五反田と蒲田が高く、その間が比較的安くなります。アルファベットの大文字のJの文字を時計回りに90度回転させたような図になります。京王井の頭線（渋谷～吉祥寺）もこのパターンです。&lt;/p&gt;
&lt;p&gt;大崎広小路は物件数が66件しかないですが、階層モデルのおかげでパラメータが推定できています（当然、95%ベイズ信用区間は広くなります）。&lt;/p&gt;
&lt;p&gt;東京メトロや都営地下鉄のように都心を横切るような路線だと真ん中あたりが高くなることもあります。&lt;/p&gt;
&lt;p&gt;階層モデルによって面積や築年数や徒歩分数要素を分離して同じ条件で比較できるようになったことで、条件を固定したときの最寄り駅による家賃の違いを知ることができました。Stan（MCMC）のおかげでパラメータの確率分布が得られるので、だいたい17.1万円-17.8万円の間という幅も分かっていいですね。家賃相場は17.4万円ですと言われても、だいたい17.1万円-17.8万円なのかだいたい15万円-20万円なのかでは話が変わってきますからね。&lt;/p&gt;
&lt;h3 id="最寄り駅別の家賃相場面積可変"&gt;最寄り駅別の家賃相場（面積可変）&lt;/h3&gt;
&lt;p&gt;築5年、徒歩5分は固定のまま、面積を20m2から60m2まで変えたときに最寄り駅によってどの程度家賃相場が変わるのか見てみましょう。$\mathrm{age}_{i} = 5, \mathrm{walk}_{i} = 5$としたときの、$\mathrm{area}_{i}$を20から60まで変えたときの$\exp(\mu_{i})$の事後中央値と95%ベイズ信用区間を計算します。&lt;/p&gt;
&lt;details&gt;
&lt;summary&gt;Code&lt;/summary&gt;
&lt;pre&gt;&lt;code class="language-r"&gt;stations &amp;lt;- c(&amp;quot;初台駅&amp;quot;, &amp;quot;幡ヶ谷駅&amp;quot;, &amp;quot;笹塚駅&amp;quot;)
stations_fct &amp;lt;- forcats::fct_relevel(as.factor(stations), stations)
idxs &amp;lt;- map_int(stations, station_to_idx)
# MCMCの取り出したサンプルが必要（上で一度実行しているのでコメントアウトで再掲）
# sample_a &amp;lt;- rstan::extract(fit, &amp;quot;a&amp;quot;)$a
# sample_b &amp;lt;- rstan::extract(fit, &amp;quot;b&amp;quot;)$b
# sample_age_b &amp;lt;- rstan::extract(fit, &amp;quot;age_b&amp;quot;)$age_b
# sample_walk_b &amp;lt;- rstan::extract(fit, &amp;quot;walk_b&amp;quot;)$walk_b
# sample_sigma_a &amp;lt;- rstan::extract(fit, &amp;quot;sigma_a&amp;quot;)$sigma_a
# sample_sigma_b &amp;lt;- rstan::extract(fit, &amp;quot;sigma_b&amp;quot;)$sigma_b
# sample_sigma &amp;lt;- rstan::extract(fit, &amp;quot;sigma&amp;quot;)$sigma
area_rent_table &amp;lt;- tidyr::expand_grid(
idx=idxs,
area=20:60,
age=5,
walk=5
) |&amp;gt;
left_join(
df_mod |&amp;gt;
filter(moyorieki_1_station %in% stations) |&amp;gt;
distinct(moyorieki_1_station, .keep_all=TRUE) |&amp;gt;
select(moyorieki_1_station, moyorieki_1_station_index) |&amp;gt;
rename(station=moyorieki_1_station, idx=moyorieki_1_station_index) |&amp;gt;
mutate(station=forcats::fct_relevel(station, stations)),
by=&amp;quot;idx&amp;quot;
) |&amp;gt;
mutate(
mu=pmap(list(idx=idx, area=area, age=age, walk=walk), \(idx, area, age, walk) {
sample_a[,idx] + sample_b[,idx]*log(area) + sample_age_b*age + sample_walk_b*(walk - 1)
})
) |&amp;gt;
mutate(
mu_exp_median=exp(map_dbl(mu, \(x) quantile(x, 0.5))),
mu_exp_lower=exp(map_dbl(mu, \(x) quantile(x, 0.025))),
mu_exp_upper=exp(map_dbl(mu, \(x) quantile(x, 0.975)))
)
area_rent_table |&amp;gt;
ggplot(aes(area, color=station, fill=station))+
theme_light()+
geom_ribbon(aes(ymin=mu_exp_lower, ymax=mu_exp_upper), alpha=0.2)+
geom_line(aes(y=mu_exp_median))+
scale_y_continuous(breaks=seq(5, 30, 5), minor_breaks=5:30)+
ggsci::scale_color_aaas()+
ggsci::scale_fill_aaas()+
theme(legend.title=element_blank())+
labs(
title=&amp;quot;exp(mu_i) (age_i=5, walk_i=5, area_i=20 - 60)&amp;quot;,
subtitle=&amp;quot;center line: estimated (median), ribbon: 95% bayesian CI&amp;quot;,
x=&amp;quot;area（面積）&amp;quot;,
y=&amp;quot;exp(mu_i)&amp;quot;,
)
&lt;/code&gt;&lt;/pre&gt;
&lt;/details&gt;
&lt;img src="images/image15-1.png" style="width:80.0%" /&gt;
&lt;p&gt;三色のバンドの中にある線は事後中央値、その上下の線は95%ベイズ信用区間です。&lt;/p&gt;
&lt;p&gt;初台、幡ヶ谷、笹塚の3駅を見ると、20m2～30m2のゾーンでは家賃はあまり変わりませんが、面積が大きくなると初台＞幡ヶ谷＞笹塚の順に家賃が変わってきます。&lt;/p&gt;
&lt;p&gt;これは結局、面積の増加に対する対数家賃の増え方のパラメータである&lt;code&gt;b&lt;/code&gt;が、初台＞幡ヶ谷＞笹塚の順に大きいからですね。以下の表は、$b_{sta[i]}$の事後中央値（表のb）、95%ベイズ信用区間（.lower,
.upper）です。&lt;/p&gt;
&lt;details&gt;
&lt;summary&gt;Code&lt;/summary&gt;
&lt;pre&gt;&lt;code class="language-r"&gt;# print(fit, pars=c(&amp;quot;b[89]&amp;quot;, &amp;quot;b[173]&amp;quot;, &amp;quot;b[353]&amp;quot;))
tidy_draws_by_idx |&amp;gt;
filter(idx %in% idxs) |&amp;gt;
left_join(
df_mod |&amp;gt;
filter(moyorieki_1_station %in% stations) |&amp;gt;
distinct(moyorieki_1_station, .keep_all=TRUE) |&amp;gt;
select(moyorieki_1_station, moyorieki_1_station_index) |&amp;gt;
rename(station=moyorieki_1_station, idx=moyorieki_1_station_index) |&amp;gt;
mutate(station=forcats::fct_relevel(station, stations)),
by=&amp;quot;idx&amp;quot;
) |&amp;gt;
ungroup() |&amp;gt;
group_by(station) |&amp;gt;
tidybayes::median_qi(b, .width=0.95) |&amp;gt;
select(station, b, .lower, .upper)
&lt;/code&gt;&lt;/pre&gt;
&lt;/details&gt;
&lt;pre&gt;&lt;code&gt;#&amp;gt; # A tibble: 3 × 4
#&amp;gt; station b .lower .upper
#&amp;gt; &amp;lt;fct&amp;gt; &amp;lt;dbl&amp;gt; &amp;lt;dbl&amp;gt; &amp;lt;dbl&amp;gt;
#&amp;gt; 1 初台駅 0.883 0.858 0.907
#&amp;gt; 2 幡ヶ谷駅 0.836 0.814 0.859
#&amp;gt; 3 笹塚駅 0.791 0.770 0.812
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;なので、面積が大きくなるほど家賃相場が開いていきます。&lt;/p&gt;
&lt;p&gt;&lt;a href="https://note.com/hanaori/n/na2e41f1d3b49" target="_blank" rel="noopener noreferrer"&gt;R｜階層線形モデルで渋谷区の賃貸価格を予想する｜hanaori&lt;/a&gt;の記事にインスピレーションをいただいた内容です。&lt;/p&gt;
&lt;h2 id="おわりに"&gt;おわりに&lt;/h2&gt;
&lt;p&gt;任意の築年数、徒歩分数、面積、最寄り駅のもとでの家賃相場を推定できました。Stanではドメイン知識を活かして考えたモデルをそのままコードに落として推定や解釈ができるので楽しいですね。住みたい駅を見つけるのに使えそうです。&lt;/p&gt;
&lt;p&gt;わたし自身が知りたかった内容を知ることができたのでよかったです。個人開発は自分が使いたいものを作ろうという話がありますが、まさにその通りでした。&lt;/p&gt;
&lt;p&gt;今回のモデルでは、築年数と徒歩分数が対数家賃を押し下げる効果は線形でかつ一律としました。しかし、新築や徒歩1～2分のような物件は、築数年や徒歩5分程度の物件と比べてさらに高い可能性もあります。新築のプレミアムのようなイメージですね。&lt;/p&gt;
&lt;p&gt;また、減価効果は駅によっても違うかもしれません。特に都心の真ん中の新築物件や駅近物件は、他の駅の新築物件や駅近物件よりもプレミアムが乗る可能性があります。一方で、同じ駅近でも地下鉄ではなく地上を走る路線沿いや大きな駅の近くの場合は、騒音などの影響でプレミアムが乗らない可能性もあります。&lt;/p&gt;
&lt;p&gt;築年数による減価効果は建物の材質や駅からの徒歩距離によっても変わりそうです。木造より鉄筋の方が、また駅から遠い物件より駅近物件の方が、築年数が経過しても価値が下がりにくいかもしれません。&lt;/p&gt;
&lt;p&gt;しかし、全体の傾向をつかむという目的のモデルでは、一律でもそこまで悪くないだろうということで定式化しました（今回は建物の材質のデータは取れていないこともあります）。&lt;/p&gt;
&lt;p&gt;他にも特徴量を追加することでさらに説明力のあるモデルが作れそうです。Future
Work、今後の課題ということで以下に示します。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;最寄り駅を複数考慮
&lt;ul&gt;
&lt;li&gt;今回は最大で3つある最寄り駅のうち1番上に書いてあるものしか使っていない&lt;/li&gt;
&lt;li&gt;同じ幡ヶ谷が最寄り駅でも、初台寄りの幡ヶ谷と笹塚寄りの幡ヶ谷だと平均的には前者の方がすこし高そう。最寄り駅の情報を複数使うと、最寄り駅効果をよりよく推定できそう
&lt;ul&gt;
&lt;li&gt;地図は二次元平面なので二次元の位置関係を考慮するということ&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;物件の階数や建物の高さ
&lt;ul&gt;
&lt;li&gt;1階は安く最上階は高い&lt;/li&gt;
&lt;li&gt;物件の階数が上がるほど高い&lt;/li&gt;
&lt;li&gt;階数が高い建物はその分設備が豪華になる傾向にありそうなので家賃も上がりそう&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;物件の構造や設備の有無
&lt;ul&gt;
&lt;li&gt;鉄筋なら高そうだし、バストイレ別とか角部屋だとやはり高そう&lt;/li&gt;
&lt;li&gt;鉄筋か木造かのような物件の構造は築年数による減価効果に影響がありそう&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;近所の店舗情報と駅の位置関係
&lt;ul&gt;
&lt;li&gt;スーパーマーケットが近所100mか500mかで違いそうだし、同じ100mでも、駅 -
スーパー - 物件という位置関係の方が、駅 - 物件 -
スーパーという位置関係よりも家賃が高そう（駅からの帰りに寄れる）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="参考にした記事など"&gt;参考にした記事など&lt;/h2&gt;
&lt;h3 id="先行研究"&gt;先行研究&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://statmodeling.hatenablog.com/entry/wariyasu-chintai" target="_blank" rel="noopener noreferrer"&gt;データ解析で割安賃貸物件を探せ！（山手線沿線編） - StatModeling
Memorandum&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://note.com/hanaori/n/na2e41f1d3b49" target="_blank" rel="noopener noreferrer"&gt;R｜階層線形モデルで渋谷区の賃貸価格を予想する｜hanaori&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pompom168.hatenablog.com/entry/2017/12/04/145016" target="_blank" rel="noopener noreferrer"&gt;回帰分析と機械学習で中央線の高コスパ物件を探す（スクレイピング+前処理） -
sola&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.analyze-world.com/entry/2017/11/09/061023" target="_blank" rel="noopener noreferrer"&gt;機械学習を使って東京23区のお買い得賃貸物件を探してみた -
データで見る世界&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://gri.jp/media/entry/8771" target="_blank" rel="noopener noreferrer"&gt;【ForecastFlow×LLoco】機械学習を使って会社近くのお得物件をSUUMOから探し出せ　〜（１）問題設定・データ取得編〜｜CO-WRITE&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://akatak.hatenadiary.jp/entry/2018/08/12/093126" target="_blank" rel="noopener noreferrer"&gt;Pythonによる不動産情報のデータ取得＆分析（１）【賃貸物件／Webスクレイピング編】 -
akatak blog&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://myfrankblog.com/machine-learning-analysis-for-real-states-data/" target="_blank" rel="noopener noreferrer"&gt;【Pythonで不動産データ分析！】機械学習(ランダムフォレスト)を用いてSUUMOからお得物件を探してみた！&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="bayesplotのプロット"&gt;bayesplotのプロット&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://kosugitti.net/archives/6078" target="_blank" rel="noopener noreferrer"&gt;Bayesplotパッケージはいいよ – Kosugitti’s
BLOG&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dastatis.github.io/pdf/StanAdvent2018/bayesplot.html" target="_blank" rel="noopener noreferrer"&gt;Introduction to bayesplot (mcmc_
series)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://das-kino.hatenablog.com/entry/2018/12/10/211511" target="_blank" rel="noopener noreferrer"&gt;Introduction to bayesplot (ppc_ series) -
nora_goes_far&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ill-identified.hatenablog.com/entry/2019/06/13/010510" target="_blank" rel="noopener noreferrer"&gt;[R] [stan] bayesplot を使ったモンテカルロ法の実践ガイド -
ill-identified
diary&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="書籍"&gt;書籍&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.amazon.co.jp/dp/4320112423" target="_blank" rel="noopener noreferrer"&gt;StanとRでベイズ統計モデリング (Wonderful
R)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="footnotes" role="doc-endnotes"&gt;
&lt;hr&gt;
&lt;ol&gt;
&lt;li id="fn:1"&gt;
&lt;p&gt;単純に平均するのは、「その街の家賃相場」は実際にある物件の築年数を含めたものとして算出するという考えによるので、どちらが正しいということではありません。&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:2"&gt;
&lt;p&gt;家賃10万円で敷金1ヶ月礼金1ヶ月（＝2年で260万円）の物件と、家賃10.5万円で敷金礼金なし（＝2年で252万円）の物件なら、他の全ての条件が同じなら後者を選びそうですが、分かりやすくするため&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:3"&gt;
&lt;p&gt;&lt;a href="https://cdn.p.recruit.co.jp/terms/suu-t-1003/index.html" target="_blank" rel="noopener noreferrer"&gt;SUUMOの利用規約&lt;/a&gt;を読んだところ、スクレイピングを特に禁じてはいないと理解しています。&amp;#160;&lt;a href="#fnref:3" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:4"&gt;
&lt;p&gt;この後expをかけて対数を外したパラメータを示しますが、medianは変数変換に依存しないからです。&amp;#160;&lt;a href="#fnref:4" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:5"&gt;
&lt;p&gt;テイラー展開より、$x=0$のまわりなら$(1-x)^n \approx -nx$なので$-nx$で近似できるということです。&amp;#160;&lt;a href="#fnref:5" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:6"&gt;
&lt;p&gt;正確には、$N(\mu_{i}, \sigma)$から生成される$\log{y_{i}}$のexpを取ったもの&amp;#160;&lt;a href="#fnref:6" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</description></item><item><title>TOPIXのボラティリティをStochastic Volatilityモデル + R + Stanで推定する</title><link>https://suzunano.net/posts/stochastic-volatility-model/</link><pubDate>Tue, 19 Dec 2023 00:00:00 +0900</pubDate><guid>https://suzunano.net/posts/stochastic-volatility-model/</guid><description>&lt;h2 id="はじめに"&gt;はじめに&lt;/h2&gt;
&lt;p&gt;この記事は&lt;a href="https://qiita.com/advent-calendar/2023/market-api" target="_blank" rel="noopener noreferrer"&gt;マケデコ Advent Calendar
2023&lt;/a&gt;の18日目の記事です。1日遅れですが、枠が空いていたので飛び入り参加してみました。&lt;/p&gt;
&lt;p&gt;状態空間モデルによるボラティリティモデルのStochastic
Volatilityの論文をR +
Stanで実装してみました。Stanの推定結果のプロットにはbayesplotとggplot2を用いています。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;非線形な状態空間モデルによるボラティリティモデルであるStochastic
Volatility (SV)
モデルをStanで実装することで、TOPIXのボラティリティを推定しました。&lt;/li&gt;
&lt;li&gt;推定されたボラティリティは、2008年のリーマンショックと、2011年の東日本大震災、2020年の新型コロナウイルスによる市場の急落局面で非常に高まっていることを確認できました。&lt;/li&gt;
&lt;li&gt;ボラティリティが一度高まるとしばらくボラティリティが高い日が続く現象であるボラティリティ・クラスタリングも確認できました。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="ボラティリティの定式化"&gt;ボラティリティの定式化&lt;/h2&gt;
&lt;p&gt;ボラティリティとは、金融商品の値動きの変動の大きさを指すパラメータです。どのくらいの損失がどのくらいの確率で発生するかを示すVaR（Value
at Risk）に使われるなど、金融工学の根幹をなす値です。&lt;/p&gt;
&lt;p&gt;以下は株式に限らず、例えば為替など、金融商品であれば何でもよいですが、株式とします。&lt;/p&gt;
&lt;p&gt;$S_t$を$t (1, \dots,\ T)$日における株式の価格とするとき、$t$日における対前日の収益率$r_t$は$r_t=\log S_t - \log S_{t-1}$となります。これを対数収益率といいます。&lt;/p&gt;
&lt;p&gt;このとき、ボラティリティとは以下の$\sigma_t$、あるいは$\sigma_t^2$を指します&lt;sup id="fnref:1"&gt;&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref"&gt;1&lt;/a&gt;&lt;/sup&gt;。&lt;/p&gt;
&lt;p&gt;$$
\begin{aligned}
r_t &amp;amp;= E_{t-1}[r_t] + \epsilon_t \\\
\epsilon_t &amp;amp;= \sigma_t z_t, \quad \sigma_t &amp;gt; 0, \quad z_t \sim i.i.d., \quad E[z_t] = 0, \quad Var[z_t] = 1
\end{aligned}
$$&lt;/p&gt;
&lt;p&gt;$\sigma_t$と$\sigma_t^2$のどちらをボラティリティと呼ぶかは文献によりますが、以降$\sigma_t$をボラティリティと呼びます。&lt;/p&gt;
&lt;p&gt;この$\sigma_t$を推定する方法は大きく分けて三通りあります。&lt;/p&gt;
&lt;p&gt;一つ目の方法は、$\sigma_t$を過去一定期間の$r_t$の標準偏差とする方法です。過去1年～3年＝250～750営業日ということで$j=250$や$j=750$などとして、$\{r_{t-j+1},r_{t-j+2},\dots r_{t}\}$の標準偏差とします。もちろん直近のボラティリティを重視したいなら$j$をより小さい値に設定します。この$t$を1日ずつずらしてローリング計算することでボラティリティの時系列を得ることができます。&lt;/p&gt;
&lt;p&gt;この方法はシンプルですが、標準偏差を計算している$t-j+1,\dots,t$の間は$\sigma_t$が一定であることを暗に仮定しています。実際はそうではありませんので、精緻に求めるなら残りの二つの方法のどちらかを用いることになります。&lt;/p&gt;
&lt;p&gt;二つ目の方法は、$\sigma_t$を統計的なモデルで定式化するものです。&lt;/p&gt;
&lt;p&gt;このタイプのモデルは有名なGARCHモデルをはじめ色々ありますが、本記事ではStochastic
Volatility (SV)
モデルと呼ばれるモデルのうち、シンプルな以下のモデルを推定することにします。&lt;/p&gt;
&lt;p&gt;$$
\begin{aligned}
y_t &amp;amp;= \exp(x_t/2) \epsilon_t, \quad \epsilon_t \sim N(0,1) \\\
x_{t+1} &amp;amp;= \mu + \phi(x_t - \mu) + \eta_t, \quad \eta_t \sim N(0,\sigma_{\eta}^2) \\\
x_1 &amp;amp; \sim N(0,\sigma_{\eta}^2/(1-\phi^2))
\end{aligned}
$$&lt;/p&gt;
&lt;p&gt;ここで$y_t$は対数収益率$r_t$であり、$\exp(x_t/2)$がボラティリティ$\sigma_t$です。$x_t$は定常な過程と仮定し、$|\phi|&amp;lt;1$です。&lt;/p&gt;
&lt;p&gt;このSVモデルは、最初の式で$E_{t-1}[r_t]=0$とした上で、$x_t = \log \sigma_t^2$がAR(1)モデルに従うことを意味します。&lt;/p&gt;
&lt;p&gt;このモデルは1本目の式が観測方程式、2本目の式が状態方程式の非線形な状態空間モデルですので、パーティクルフィルタのような非線形な状態空間モデルでも推定できるタイプのフィルタ系の手法か、Stanなどを用いてMCMCで推定することになります。&lt;/p&gt;
&lt;p&gt;最後に三つ目の方法としては、以上二つのように収益率$r_t$の時系列から$\sigma_t$を推定するのではなく、分単位のような細かい収益率データを用いて直接$\sigma_t$を求めるアプローチがあります。本記事からは外れるので詳細は触れませんが、$t$日における1分間隔や5分間隔程度の細かい間隔の収益率の2乗を1日分足し合わせたものが$\sigma_t$の精度のよい推定量（Realized
Volatility&lt;sup id="fnref:2"&gt;&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref"&gt;2&lt;/a&gt;&lt;/sup&gt;と呼びます）になることが知られています。&lt;/p&gt;
&lt;h2 id="環境"&gt;環境&lt;/h2&gt;
&lt;p&gt;R +
RStanです。Stanコードを書いてrstanというRのパッケージから呼び出します。&lt;/p&gt;
&lt;p&gt;Rを用いているのはベイズモデリングは文献もパッケージもRが豊富なためです。あとこの記事のコードは自分で昔書いたコードを流用しているのですが、それがRだったからというのもあります。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Windows 10&lt;/li&gt;
&lt;li&gt;R 4.3.1&lt;/li&gt;
&lt;li&gt;httr 1.4.7&lt;/li&gt;
&lt;li&gt;rstan 2.32.3&lt;/li&gt;
&lt;li&gt;bayesplot 1.10.0&lt;/li&gt;
&lt;li&gt;patchwork 1.1.3&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class="language-r"&gt;library(tidyverse)
library(httr)
library(rstan)
library(bayesplot)
library(patchwork)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id="データの取得"&gt;データの取得&lt;/h2&gt;
&lt;p&gt;今回は上で挙げたSVモデルをRとStan
(RStan)で実装し、MCMCによってパラメータを推定してみます。&lt;/p&gt;
&lt;p&gt;TOPIXの日次の終値が必要なので用意します。2008/5/7～2023/12/18（3824営業日）のTOPIXの終値を&lt;a href="https://jpx-jquants.com/" target="_blank" rel="noopener noreferrer"&gt;J-Quants&lt;/a&gt;から取得しました。J-QuantsはJPXが個人向けにリリースしている株価などの金融データのAPIです。&lt;/p&gt;
&lt;p&gt;この記事のモデリングをするにはTOPIXの終値だけあればいいので、J-Quantsではなくとも構いません。証券会社に口座を開いていれば、証券会社が提供しているトレーディングツールからCSVでエクスポートできたりもします。&lt;/p&gt;
&lt;details&gt;
&lt;summary&gt;
データの取得コード
&lt;/summary&gt;
&lt;p&gt;J-Quantsは、認証エンドポイントを2回POSTしてトークンを取得したら、そのトークンをBearer認証のヘッダに入れてTOPIXのエンドポイントをGETするだけのシンプルな作りです。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-r"&gt;# J-Quantsに登録したメールアドレスとパスワード
mail_address &amp;lt;- &amp;quot;MAIL_ADDRESS&amp;quot;
password &amp;lt;- &amp;quot;PASSWORD&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class="language-r"&gt;resp &amp;lt;- httr::POST(
&amp;quot;https://api.jquants.com/v1/token/auth_user&amp;quot;,
body=jsonlite::toJSON(
list(mailaddress=mail_address, password=password),
auto_unbox=TRUE
)
)
refresh_token &amp;lt;- httr::content(resp)$refreshToken
resp &amp;lt;- httr::POST(
&amp;quot;https://api.jquants.com/v1/token/auth_refresh&amp;quot;,
query=list(refreshtoken=refresh_token)
)
id_token &amp;lt;- httr::content(resp)$idToken
resp &amp;lt;- httr::GET(
&amp;quot;https://api.jquants.com/v1/indices/topix&amp;quot;,
query=list(from=&amp;quot;2008-05-07&amp;quot;, to=&amp;quot;2023-12-18&amp;quot;),
httr::add_headers(Authorization=glue::glue(&amp;quot;Bearer {id_token}&amp;quot;))
)
topix &amp;lt;- httr::content(resp)$topix |&amp;gt;
dplyr::bind_rows() |&amp;gt;
tibble::as_tibble()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最初のPOSTでは&lt;code&gt;{&amp;quot;mailaddress&amp;quot;: &amp;quot;mail@example.com&amp;quot;, &amp;quot;password&amp;quot;: &amp;quot;hoge&amp;quot;}&lt;/code&gt;のようなJSONをbodyに入れます。RでこのJSONを作るには、&lt;code&gt;jsonlite::toJSON(list(mailaddress=mail_address, password=password), auto_unbox=TRUE)&lt;/code&gt;とします。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;jsonlite::toJSON()&lt;/code&gt;の引数&lt;code&gt;auto_unbox&lt;/code&gt;はデフォルトでは&lt;code&gt;FALSE&lt;/code&gt;なのですが、&lt;code&gt;jsonlite::toJSON(list(mailaddress=mail_address, password=password), auto_unbox=FALSE)&lt;/code&gt;は&lt;code&gt;{&amp;quot;mailaddress&amp;quot;: [&amp;quot;mail@example.com&amp;quot;], &amp;quot;password&amp;quot;: [&amp;quot;hoge&amp;quot;]}&lt;/code&gt;となってしまいます。Rでは&lt;code&gt;mail@example.com&lt;/code&gt;のような文字列は長さ1のcharacter型のベクトルであるため、そのまま長さ1のリストができてしまうからです。これを防ぐために&lt;code&gt;auto_unbox=TRUE&lt;/code&gt;を指定します。&lt;/p&gt;
&lt;/details&gt;
&lt;p&gt;TOPIXの終値から対前日の対数収益率を計算します。100倍して%表記にします。また、最初の1日の収益率は&lt;code&gt;NA&lt;/code&gt;になるので最初の1日を除いておきます。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-r"&gt;df &amp;lt;- topix |&amp;gt;
mutate(Date=as.Date(Date, &amp;quot;%Y-%m-%d&amp;quot;)) |&amp;gt;
mutate(ret=(log(Close) - log(lag(Close, 1)))*100) |&amp;gt;
slice(-1)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;こんな感じのデータです。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-r"&gt;df
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;#&amp;gt; # A tibble: 3,823 × 6
#&amp;gt; Date Open High Low Close ret
#&amp;gt; &amp;lt;date&amp;gt; &amp;lt;dbl&amp;gt; &amp;lt;dbl&amp;gt; &amp;lt;dbl&amp;gt; &amp;lt;dbl&amp;gt; &amp;lt;dbl&amp;gt;
#&amp;gt; 1 2008-05-08 1384. 1386. 1373. 1373. -1.47
#&amp;gt; 2 2008-05-09 1372. 1374. 1341. 1342. -2.30
#&amp;gt; 3 2008-05-12 1331. 1345. 1327. 1343. 0.0767
#&amp;gt; 4 2008-05-13 1351. 1364. 1344. 1360. 1.28
#&amp;gt; 5 2008-05-14 1360. 1376. 1351. 1373. 0.951
#&amp;gt; 6 2008-05-15 1382. 1404. 1382. 1393. 1.43
#&amp;gt; 7 2008-05-16 1405. 1412. 1391. 1396. 0.215
#&amp;gt; 8 2008-05-19 1400. 1410. 1397. 1404. 0.599
#&amp;gt; 9 2008-05-20 1402. 1410. 1394. 1400. -0.315
#&amp;gt; 10 2008-05-21 1385. 1386. 1361. 1370. -2.15
#&amp;gt; # ℹ 3,813 more rows
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id="stanで実装"&gt;Stanで実装&lt;/h2&gt;
&lt;p&gt;上に挙げたSVモデルをStanコードで書きます。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-stan"&gt;data {
int N;
vector[N] y;
}
parameters {
vector[N] x;
real mu;
real&amp;lt;lower=-1,upper=1&amp;gt; phi;
real&amp;lt;lower=0&amp;gt; sigma_eta;
}
transformed parameters {
real phi_beta;
phi_beta = (phi + 1)/2;
real sigma_eta_square;
sigma_eta_square = sigma_eta^2;
}
model {
mu ~ normal(0, 1);
phi_beta ~ beta(20, 1.5);
sigma_eta_square ~ inv_gamma(5.0/2, 0.05/2);
x[1] ~ normal(0, sigma_eta/sqrt(1 - phi^2));
x[2:N] ~ normal(mu + phi * (x[1:(N-1)] - mu), sigma_eta);
y ~ normal(0, exp(x/2));
}
generated quantities {
vector[N] vol;
vol = exp(x/2);
vector[N] y_pred;
for (i in 1:N) {
y_pred[i] = normal_rng(0, exp(x[i]/2));
}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;以下、実装のポイントです。&lt;/p&gt;
&lt;p&gt;$\mu, \phi, \sigma_{\eta}$の事前分布は、SVモデルの元の論文であるKim,
Shephard and Chib (1998) &lt;sup id="fnref:3"&gt;&lt;a href="#fn:3" class="footnote-ref" role="doc-noteref"&gt;3&lt;/a&gt;&lt;/sup&gt;や、それを日本株に適用した大森, 渡部
(2007)&lt;sup id="fnref:4"&gt;&lt;a href="#fn:4" class="footnote-ref" role="doc-noteref"&gt;4&lt;/a&gt;&lt;/sup&gt;
にある以下の分布を用いました。IGは逆ガンマ分布です。無情報事前分布ではなく、事前分布を書いてあげると収束しやすくなります。&lt;/p&gt;
&lt;p&gt;$$
\begin{aligned}
\mu &amp;amp; \sim N(0,1) \\\
\frac{\phi+1}{2} &amp;amp; \sim Beta(20,1.5) \\\
\sigma_{\eta}^2 &amp;amp; \sim IG(5/2, 0.05/2)
\end{aligned}
$$&lt;/p&gt;
&lt;p&gt;&lt;code&gt;generated quantities&lt;/code&gt;の&lt;code&gt;vol&lt;/code&gt;が今回求めたいボラティリティ$\sigma_t$です。MCMCの結果から$x$の事後分布を取り出してR上で$\exp(x/2)$を計算することで求めることもできますが、Stanで求めておきます。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;generated quantities&lt;/code&gt;の&lt;code&gt;y_pred&lt;/code&gt;は、各パラメータの事後分布から乱数で生成した$y_t$の値です。これはパラメータの推定自体には不要ですが、あとでモデルの事後診断に使用するために求めています。&lt;/p&gt;
&lt;p&gt;あとは高速化のためにできるだけベクトル化をします。&lt;/p&gt;
&lt;p&gt;このStanコードを”svmodel.stan”で保存して以下のRコードでキックすることでMCMCの推定を行います。&lt;/p&gt;
&lt;p&gt;i9-9900Kで実行しました。chains=4, iter=110000, warmup=10000,
thin=50で8時間程度かかりました。iterをかなり大きくしてthinで間引かないと$\phi$などのパラメータに自己相関が残っていました。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-r"&gt;# Stanのおまじない（上は並列化、下はstanコードが変わらない限り再コンパイルしない）
options(mc.cores=parallel::detectCores())
rstan_options(auto_write=TRUE)
# C++コードへのコンパイル
mod &amp;lt;- rstan::stan_model(&amp;quot;svmodel.stan&amp;quot;)
# MCMCサンプリング
fit &amp;lt;- rstan::sampling(
mod,
data=list(N=nrow(df), y=df$ret),
chains=4, iter=110000, warmup=10000, thin=50, seed=1234
)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id="mcmcのチェック"&gt;MCMCのチェック&lt;/h2&gt;
&lt;p&gt;MCMCがうまくいっているかチェックします。&lt;/p&gt;
&lt;p&gt;MCMCが終わったら早速パラメータの値を見たいところですが、収束していないということがよくあるのでチェックしましょう&lt;sup id="fnref:5"&gt;&lt;a href="#fn:5" class="footnote-ref" role="doc-noteref"&gt;5&lt;/a&gt;&lt;/sup&gt;。収束していなければパラメータが正しく推定されていないということです。&lt;/p&gt;
&lt;p&gt;詳細に知りたい方は、例えばこちらの分かりやすい記事をご覧ください。&lt;/p&gt;
&lt;p&gt;&lt;a href="https://ill-identified.hatenablog.com/entry/2019/06/13/010510" target="_blank" rel="noopener noreferrer"&gt;[R] [stan] bayesplot を使ったモンテカルロ法の実践ガイド -
ill-identified
diary&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;このあたりの事後診断の可視化はbayesplotが超便利です。ggplot2ベースなので出力のグラフにggplot2の関数で軸などのレイアウトを微調整できるのもナイスです。&lt;/p&gt;
&lt;p&gt;まずはRhatです。MCMCが収束しているかの目安であり、全てのパラメータで1.1を下回ることが必要とされます。パラメータが多くて個別にプロットするのは難しいので、ヒストグラムで描きます。いい感じです。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-r"&gt;bayesplot::mcmc_rhat_hist(bayesplot::rhat(fit))
&lt;/code&gt;&lt;/pre&gt;
&lt;img src="images/image1-1.png" style="width:80.0%" /&gt;
&lt;p&gt;次に有効サンプルサイズ（n_eff）です。Nで割ったものが0.1以上であることが望ましいとされます。0.1を下回るような小さい値のパラメータは、MCMCのサンプリングで自己相関が残っていることを示唆します。自己相関が大きいとパラメータの事後分布の分散を正しく推定できません。&lt;/p&gt;
&lt;p&gt;こちらもよさそうですね。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-r"&gt;bayesplot::mcmc_neff_hist(bayesplot::neff_ratio(fit))
&lt;/code&gt;&lt;/pre&gt;
&lt;img src="images/image2-1.png" style="width:80.0%" /&gt;
&lt;p&gt;次は自己相関です。サンプルに自己相関が残っている場合、サンプルに定常性がないということになります。&lt;/p&gt;
&lt;p&gt;全部描けないので以下の3つのパラメータに絞ります。&lt;code&gt;phi&lt;/code&gt;と&lt;code&gt;sigma_eta&lt;/code&gt;にちょっと自己相関が残っていそうですが、おおむねいい感じです。もう少しiterとthinを増やせば&lt;code&gt;phi&lt;/code&gt;と&lt;code&gt;sigma_eta&lt;/code&gt;の自己相関も消える気がします。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-r"&gt;bayesplot::mcmc_acf_bar(fit, pars=c(&amp;quot;mu&amp;quot;, &amp;quot;phi&amp;quot;, &amp;quot;sigma_eta&amp;quot;))
&lt;/code&gt;&lt;/pre&gt;
&lt;img src="images/image3-1.png" style="width:80.0%" /&gt;
&lt;p&gt;ちなみに&lt;code&gt;bayesplot::mcmc_acf_bar()&lt;/code&gt;をはじめ、bayesplotの描画関数は&lt;code&gt;pars&lt;/code&gt;でパラメータを指定し忘れると全パラメータ描画します。巨大なモデルだとこれでRStudioがクラッシュすることがありますので気を付けましょう。特に時系列の状態空間モデルはパラメータの数が多いのでクラッシュしがちです（今回は&lt;code&gt;x&lt;/code&gt;が&lt;code&gt;x[1]&lt;/code&gt;から&lt;code&gt;x[3823]&lt;/code&gt;まである）。モデルをRDSファイルに保存し忘れた状態だと推定結果が消えて悲しいことになります。&lt;/p&gt;
&lt;p&gt;そしてトレースプロットを見ます。こちらもパラメータを絞っています。&lt;/p&gt;
&lt;p&gt;これはchainごとのサンプルの推移で、線が混ざり合っていると初期値によらず同じ値に収束している＝局所解に落ちていないことを示します。Rhatが大きいときはトレースプロットが混ざり合っていないので、Rhatと合わせてチェックします。いい感じですね！&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-r"&gt;bayesplot::mcmc_trace(fit, pars=c(&amp;quot;mu&amp;quot;, &amp;quot;phi&amp;quot;, &amp;quot;sigma_eta&amp;quot;))
&lt;/code&gt;&lt;/pre&gt;
&lt;img src="images/image4-1.png" style="width:80.0%" /&gt;
&lt;p&gt;最後に、モデルが現実のデータをよく説明するように定式化されているなら、パラメータの事後分布から乱数を振って得られる目的変数（ここでは$y_t$）は、実際の$y_t$と同じような分布で得られるはずです。これを図示してみます。&lt;/p&gt;
&lt;p&gt;濃い線は実際の&lt;code&gt;y_t&lt;/code&gt;（対数収益率）の分布、薄い線はモデルから生成した8000系列の対数収益率&lt;code&gt;y_pred&lt;/code&gt;のうち最初の10系列の分布です。薄い線はそれぞれの系列で10本あります。8000系列は描けないので10本に絞っています。&lt;/p&gt;
&lt;p&gt;濃い線と薄い線が大体重なっているので悪くなさそうです。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-r"&gt;bayesplot::ppc_dens_overlay(df$ret, rstan::extract(fit)$y_pred[1:10,])
&lt;/code&gt;&lt;/pre&gt;
&lt;img src="images/image5-1.png" style="width:80.0%" /&gt;
&lt;p&gt;自分でモデルを一から組む場合は、モデルのパラメータはMCMCで正しく推定されていても、そもそもモデル自体が元のデータを全然説明できていない誤ったモデルであることがあります（SVモデルは幅広く用いられているモデルなので、今回ちゃんと当てはまるのはある意味当然なわけですが）。モデルが元のデータに当てはまっているかどうかを示してくれるのが上のプロットです。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;bayesplot::ppc_*&lt;/code&gt;系の関数はこのような事後予測の確認に役立つものが色々あって便利です。&lt;/p&gt;
&lt;p&gt;以上がMCMCの結果の基本的なチェック方法です。&lt;/p&gt;
&lt;h2 id="パラメータの推定結果"&gt;パラメータの推定結果&lt;/h2&gt;
&lt;p&gt;以下がパラメータの推定結果です。ただしprintが長くなるので一部のパラメータに絞っています。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-r"&gt;print(fit, pars=c(&amp;quot;mu&amp;quot;, &amp;quot;phi&amp;quot;, &amp;quot;sigma_eta&amp;quot;), digits_summary=3)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;#&amp;gt; Inference for Stan model: anon_model.
#&amp;gt; 4 chains, each with iter=110000; warmup=10000; thin=50;
#&amp;gt; post-warmup draws per chain=2000, total post-warmup draws=8000.
#&amp;gt;
#&amp;gt; mean se_mean sd 2.5% 25% 50% 75% 97.5% n_eff Rhat
#&amp;gt; mu 0.127 0.001 0.106 -0.079 0.058 0.127 0.197 0.336 7883 1
#&amp;gt; phi 0.966 0.000 0.007 0.951 0.961 0.966 0.971 0.978 4028 1
#&amp;gt; sigma_eta 0.213 0.000 0.020 0.176 0.198 0.212 0.226 0.255 3167 1
#&amp;gt;
#&amp;gt; Samples were drawn using NUTS(diag_e) at Wed Dec 20 00:41:18 2023.
#&amp;gt; For each parameter, n_eff is a crude measure of effective sample size,
#&amp;gt; and Rhat is the potential scale reduction factor on split chains (at
#&amp;gt; convergence, Rhat=1).
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;推定したボラティリティを見てみましょう。&lt;code&gt;rstan::extract()&lt;/code&gt;を用いてパラメータの事後分布のサンプルから&lt;code&gt;vol&lt;/code&gt;の時系列を作ります。&lt;/p&gt;
&lt;p&gt;点推定値として事後中央値を用います。あわせて95%ベイズ信用区間も示したいので、中央値と2.5%タイル点と97.5%タイル点を取り出します。&lt;code&gt;vol&lt;/code&gt;の事後分布の&lt;code&gt;(110000 (iter) - 10000 (warmup))/50 (thin) * 4 (chain) = 8000&lt;/code&gt;個のサンプルを小さい順に並び変えて50%と2.5%と97.5%のタイル点を取り出すことで得られます。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-r"&gt;# 8000 (サンプル) x 3823 (vol[1] - vol[3823])のmatrix
mat &amp;lt;- rstan::extract(fit, &amp;quot;vol&amp;quot;)$vol
vol_stat &amp;lt;- tibble::tibble(
vol_median=apply(mat, 2, \(x) quantile(x, 0.5)),
vol_lower=apply(mat, 2, \(x) quantile(x, 0.025)),
vol_upper=apply(mat, 2, \(x) quantile(x, 0.975))
) |&amp;gt;
mutate(Date=df$Date) |&amp;gt;
relocate(Date)
res &amp;lt;- left_join(df, vol_stat, by=&amp;quot;Date&amp;quot;)
res
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;#&amp;gt; # A tibble: 3,823 × 9
#&amp;gt; Date Open High Low Close ret vol_median vol_lower vol_upper
#&amp;gt; &amp;lt;date&amp;gt; &amp;lt;dbl&amp;gt; &amp;lt;dbl&amp;gt; &amp;lt;dbl&amp;gt; &amp;lt;dbl&amp;gt; &amp;lt;dbl&amp;gt; &amp;lt;dbl&amp;gt; &amp;lt;dbl&amp;gt; &amp;lt;dbl&amp;gt;
#&amp;gt; 1 2008-05-08 1384. 1386. 1373. 1373. -1.47 1.36 0.892 2.20
#&amp;gt; 2 2008-05-09 1372. 1374. 1341. 1342. -2.30 1.37 0.908 2.15
#&amp;gt; 3 2008-05-12 1331. 1345. 1327. 1343. 0.0767 1.34 0.894 2.10
#&amp;gt; 4 2008-05-13 1351. 1364. 1344. 1360. 1.28 1.33 0.896 2.10
#&amp;gt; 5 2008-05-14 1360. 1376. 1351. 1373. 0.951 1.33 0.894 2.06
#&amp;gt; 6 2008-05-15 1382. 1404. 1382. 1393. 1.43 1.33 0.892 2.05
#&amp;gt; 7 2008-05-16 1405. 1412. 1391. 1396. 0.215 1.32 0.887 2.07
#&amp;gt; 8 2008-05-19 1400. 1410. 1397. 1404. 0.599 1.33 0.903 2.05
#&amp;gt; 9 2008-05-20 1402. 1410. 1394. 1400. -0.315 1.36 0.918 2.06
#&amp;gt; 10 2008-05-21 1385. 1386. 1361. 1370. -2.15 1.39 0.963 2.09
#&amp;gt; # ℹ 3,813 more rows
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;以下のグラフの上はボラティリティ$\sigma_t$（赤線は$\sigma_{t}$の事後分布の中央値、青いバンドは95%ベイズ信用区間）、下はTOPIXの終値です。$\sigma_t = a$であれば、TOPIXの収益率の標準偏差がa[%]であることを意味します。&lt;/p&gt;
&lt;details&gt;
&lt;summary&gt;code&lt;/summary&gt;
&lt;pre&gt;&lt;code class="language-r"&gt;p_vol &amp;lt;- res |&amp;gt;
ggplot(aes(Date))+
theme_light()+
geom_ribbon(aes(ymin=vol_lower, ymax=vol_upper), fill=&amp;quot;lightsteelblue1&amp;quot;, alpha=0.5)+
geom_line(aes(y=vol_upper), color=&amp;quot;lightsteelblue1&amp;quot;)+
geom_line(aes(y=vol_lower), color=&amp;quot;lightsteelblue1&amp;quot;)+
geom_line(aes(y=vol_median), color=&amp;quot;firebrick&amp;quot;)+
scale_x_date(breaks=scales::date_breaks(&amp;quot;1 year&amp;quot;), date_labels=&amp;quot;%y&amp;quot;)+
labs(
x=&amp;quot;date (year)&amp;quot;,
y=&amp;quot;volatility (sigma_t)&amp;quot;,
subtitle=&amp;quot;red: estimated (median), light blue: 95% CI&amp;quot;
)
p_topix &amp;lt;- res |&amp;gt;
ggplot(aes(x=Date, y=Close))+
theme_light()+
geom_line()+
scale_x_date(breaks=scales::date_breaks(&amp;quot;1 year&amp;quot;), date_labels=&amp;quot;%y&amp;quot;)+
labs(x=&amp;quot;date (year)&amp;quot;, y=&amp;quot;TOPIX close&amp;quot;)
patchwork::wrap_plots(p_vol, p_topix, ncol=1)
&lt;/code&gt;&lt;/pre&gt;
&lt;/details&gt;
&lt;img src="images/image6-1.png" style="width:80.0%" /&gt;
&lt;p&gt;2008年のリーマンショック、2011年の東日本大震災、2020年の新型コロナウイルスの市場急落局面でボラティリティが高まっていることが分かります。&lt;/p&gt;
&lt;p&gt;なお、グラフはpatchworkで並べました。複数のggplotオブジェクトを綺麗に並べられて重宝するパッケージです。&lt;/p&gt;
&lt;p&gt;最後に$\phi$のパラメータの事後分布を見てみます。先程示した通り、$\phi$ =
0.966 (95%CI: 0.951-0.978)でした。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-r"&gt;bayesplot::mcmc_hist(fit, pars=&amp;quot;phi&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;img src="images/image7-1.png" style="width:80.0%" /&gt;
&lt;p&gt;このパラメータは過去のボラティリティがどの程度後を引くかを示すパラメータであり、1に近いということは持続性がかなりあるということを示します。ボラティリティが一度上昇するとしばらくボラティリティが高い日が続くということであり、この現象をボラティリティ・クラスタリングといいます。&lt;/p&gt;
&lt;p&gt;SVモデルを推定した論文をサーベイすると$\phi$の推定値は0.8から0.995までの値となっているという論文がありますが&lt;sup id="fnref:6"&gt;&lt;a href="#fn:6" class="footnote-ref" role="doc-noteref"&gt;6&lt;/a&gt;&lt;/sup&gt;、この先行研究と整合的です。&lt;/p&gt;
&lt;h2 id="おわりに"&gt;おわりに&lt;/h2&gt;
&lt;p&gt;Stanを使うと柔軟にモデルを組んで解釈ができて楽しいですね。&lt;/p&gt;
&lt;p&gt;SVモデルの一番シンプルなものを推定してみましたが、SVモデルには収益率の裾の厚さを表現するために誤差項をt分布としたり、収益率がマイナスの日の方がボラティリティが高まる（ボラティリティの非対称性といいます）ことを表現したりするモデルなど、色々な発展形があります。これらをStanで組んでみてもよいでしょう。&lt;/p&gt;
&lt;p&gt;最後に株価データで時系列モデリングをやってみたいという方におすすめの書籍をご紹介します。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.amazon.co.jp/dp/4254275048" target="_blank" rel="noopener noreferrer"&gt;ボラティリティ変動モデル (シリーズ
現代金融工学)&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;統計モデル的アプローチによるボラティリティモデルです。ボラティリティモデルをしっかり学ぶならこれです。MCMCによる推定も少し触れられています。コード例はなく数式展開の理論の本です。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.amazon.co.jp/dp/425412841X" target="_blank" rel="noopener noreferrer"&gt;経済・ファイナンスのための カルマンフィルター入門
(統計ライブラリー)&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;前半はカルマンフィルタの解説、後半は経済・金融データへのカルマンフィルタの適用事例の紹介です。前半はカルマンフィルタの導出を丁寧に書いていて理解しやすかったです。後半はカルマンフィルタでベータ値を推定したりペアトレーディングの銘柄を発掘したりと面白い事例が豊富です。コードはないですが読みやすいです。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="関連記事"&gt;関連記事&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="../stock-beta/"&gt;カルマンフィルタで株式のベータ値を推定する - suzuna's memo&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="footnotes" role="doc-endnotes"&gt;
&lt;hr&gt;
&lt;ol&gt;
&lt;li id="fn:1"&gt;
&lt;p&gt;式の通り、$z_t$は正規分布とは指定していないので、$r_t$も正規分布以外の分布を仮定することができます。実証的には$r_t$は裾が厚く正規分布ではないとされます。&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:2"&gt;
&lt;p&gt;例えばこの辺りの論文をご覧ください。渡部敏明, 佐々木浩二 (2006),
「ARCH型モデルと”Realized
Volatility”によるボラティリティ予測とバリュー・アット・リスク」,
金融研究, 25 別冊(2), 39-74.&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:3"&gt;
&lt;p&gt;Kim, S., N. Shephard, and S. Chib (1998), “Stochastic Volatility:
Likelihood Inference and Comparison with ARCH Models”, Review of
Economic Studies, 65, 361-393.&amp;#160;&lt;a href="#fnref:3" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:4"&gt;
&lt;p&gt;大森裕浩, 渡部敏明 (2007),
「MCMC法とその確率的ボラティリティモデルへの応用」CIRJEディスカッションペーパー,
J-173, 1-39.&amp;#160;&lt;a href="#fnref:4" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:5"&gt;
&lt;p&gt;何日もかけて回したMCMCの結果をチェックしたら全然収束していない悲しみもあるある&amp;#160;&lt;a href="#fnref:5" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:6"&gt;
&lt;p&gt;Jacquier, E., N. Polson, and P. Rossi (2004), “Bayesian Analysis
of Stochastic Volatility Models (with Discussion)”, Journal of
Business &amp;amp; Economic Statistics, 12, 371-417.&amp;#160;&lt;a href="#fnref:6" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</description></item><item><title>GitHub ActionsでHugoのビルドを自動化する</title><link>https://suzunano.net/posts/hugo-build/</link><pubDate>Tue, 17 Oct 2023 00:00:00 +0900</pubDate><guid>https://suzunano.net/posts/hugo-build/</guid><description>&lt;h2 id="概要"&gt;概要&lt;/h2&gt;
&lt;p&gt;Hugoで作ったブログをGitHub Pagesで公開するときに、HugoのビルドをGitHub Actionsで行うことでビルドを自動化します。&lt;/p&gt;
&lt;p&gt;GitHubのリポジトリはHugoのソース用のリポジトリと、Hugoのビルドの生成物のリポジトリの二つ作っている場合を考えます。前者は下の構成図でsuzuna/blog-source、後者はsuzuna/blogにあたります。&lt;/p&gt;
&lt;p&gt;このとき、前者に&lt;code&gt;git push&lt;/code&gt;すると、Hugoでビルドし、生成物を後者にpushするようなGitHub Actionsを前者のリポジトリで動かすということです。&lt;/p&gt;
&lt;img src="architecture.png"&gt;
&lt;h2 id="hugoのビルド自動化リポジトリ分割のメリット"&gt;Hugoのビルド自動化×リポジトリ分割のメリット&lt;/h2&gt;
&lt;p&gt;HugoのビルドをGitHub Actionsで自動化するメリットは、主に二つあると思います。&lt;/p&gt;
&lt;p&gt;記事を書き上げてGitHub Pagesで公開する前に、ローカルPCでHugoのビルドのコマンドを実行する必要がないのが一つです。&lt;code&gt;hugo&lt;/code&gt;でビルドするのを忘れてpushすることがよくあるんですよね。&lt;/p&gt;
&lt;p&gt;また、Hugoのビルド環境が統一されるのがもう一つのメリットです。複数のPCで記事を書くような場合でも環境の差異に悩むことがなくなりますし、Markdownが書ければよいのでHugoを入れていない端末でも記事を書けます。&lt;/p&gt;
&lt;p&gt;次にHugoのソースと生成物を別々のリポジトリに分割することで、ソースと生成物が同じリポジトリに混ざらないためGit上で差分が見やすくなります。記事やHugoのThemeのテンプレートを編集するときなどに、ソースとビルドの生成物の差分が混ざらないメリットをよく実感できます。このリポジトリ分割は一般的な方法なのか分かりませんが（ググるとちらほら出てきます）、Git管理上快適だと感じています。&lt;/p&gt;
&lt;h2 id="github-actionsのトークンの作成"&gt;GitHub Actionsのトークンの作成&lt;/h2&gt;
&lt;p&gt;ここから実際の方法を説明します。&lt;/p&gt;
&lt;p&gt;GitHub Actionsを実行するリポジトリから別のリポジトリにpushし、pushしたリポジトリでGitHub Pagesとして公開するGitHub Actionとして、&lt;a href="https://github.com/peaceiris/actions-gh-pages" target="_blank" rel="noopener noreferrer"&gt;peaceiris/actions-gh-pages&lt;/a&gt;を用います。&lt;/p&gt;
&lt;p&gt;このGitHub Actionsは、同一のリポジトリにpushしてGitHub Pagesとして公開することもできます。GitHubのReadmeにある通り、この場合は特段GitHubのトークンは必要ではないのですが、今回のように別のリポジトリにpushする際はGitHubのトークンを渡す必要があります。&lt;/p&gt;
&lt;p&gt;そのため、最初にssh鍵である&lt;code&gt;deploy_key&lt;/code&gt;か、Personal Access Tokenである&lt;code&gt;personal_token&lt;/code&gt;のどちらかを用意する必要があります。以下のどちらかを行ってください。&lt;/p&gt;
&lt;h3 id="トークンの作成deploy_key"&gt;トークンの作成（deploy_key）&lt;/h3&gt;
&lt;p&gt;ssh鍵を作ります。GitHubにssh接続する際にも作るやつですね。&lt;a href="https://docs.github.com/ja/authentication/connecting-to-github-with-ssh/generating-a-new-ssh-key-and-adding-it-to-the-ssh-agent" target="_blank" rel="noopener noreferrer"&gt;GitHubの公式ドキュメント&lt;/a&gt;の通りなのですが、簡単に説明します。&lt;/p&gt;
&lt;p&gt;まずGit Bashで以下を実行します。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;ssh-keygen -t ed25519 -C &amp;quot;&amp;lt;GitHubに登録したメールアドレス&amp;gt;&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;これを実行すると、以下の3項目を入力するようメッセージが出ますので、順に入力します。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&amp;ldquo;Enter file in which to save the key&amp;rdquo;: ssh鍵に付けるファイル名を入力します。&lt;/li&gt;
&lt;li&gt;&amp;ldquo;Enter passphrase (empty for no passphrase)&amp;rdquo; 鍵のパスフレーズです。emptyでいいので何も入力せずEnterを押します。&lt;/li&gt;
&lt;li&gt;&amp;ldquo;Enter same passphrase again&amp;rdquo;: 上と同じ値を入力します。こちらも同じく何も入力せずEnterを入力します。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;1個目で何も入力しなければ、&lt;code&gt;id_ed25519&lt;/code&gt;（秘密鍵）と&lt;code&gt;id_ed25519.pub&lt;/code&gt;（公開鍵）の2つの鍵が生成されています。&lt;/p&gt;
&lt;p&gt;そうしたら、ソース管理用のGitHubリポジトリ（構成図のsuzuna/blog-source）をブラウザで開き、Settings &amp;gt; Secrets &amp;gt; Actions &amp;gt; New repository secretを開きます。NameとValueを入力する欄があります。NameはGitHub Actions上で参照するSecretの名前です。任意の文字列でよいですが、ここでは&lt;code&gt;ACTIONS_DEPLOY_KEY&lt;/code&gt;とします。Valueは秘密鍵の中身を貼り付けます。後者は&lt;code&gt;id_ed25519&lt;/code&gt;をテキストエディタで開いて中身を全部コピーして貼り付ければOKです。&lt;/p&gt;
&lt;p&gt;次に、Hugoの生成物のリポジトリ（構成図のsuzuna/blog）をブラウザで開き、Settings &amp;gt; Deploy keys &amp;gt; Add deploy keyを開きます。Titleは任意の名前を設定します。Keyは公開鍵の中身を貼り付けます。最後に&lt;code&gt;Allow write access&lt;/code&gt;にチェックを入れます。&lt;/p&gt;
&lt;h3 id="トークンの作成personal_token"&gt;トークンの作成（personal_token）&lt;/h3&gt;
&lt;p&gt;GitHubをブラウザで開き、Account SettingsのSettings &amp;gt; Developer settings &amp;gt; Personal access tokens &amp;gt; Generate new tokenを開きます。Personal Access TokenのScopeとして&lt;code&gt;repo&lt;/code&gt;と&lt;code&gt;workflow&lt;/code&gt;にチェックを入れます。Tokenが表示されるので控えておきます。&lt;/p&gt;
&lt;p&gt;そうしたら、deploy_keyの場合と同様に、ソース管理用のGitHubリポジトリのrepository secretを開き、Nameには任意の名前（ここでは&lt;code&gt;PERSONAL_TOKEN&lt;/code&gt;とします）、Valueには先程のTokenを入力します。&lt;/p&gt;
&lt;h2 id="github-actionsの作成"&gt;GitHub Actionsの作成&lt;/h2&gt;
&lt;p&gt;次に、GitHub Actionsのymlファイルを作成します。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;deploy_key&lt;/code&gt;を用いる場合は、下記をソース管理用のリポジトリに&amp;quot;.github/workflows/&amp;lt;任意のファイル名&amp;gt;.yml&amp;quot;で保存してcommitします。&lt;code&gt;on&lt;/code&gt;に記載の通り、masterにpushするとGitHub Actionsが実行されます。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-yaml"&gt;name: github-pages
on:
push:
branches:
- master
jobs:
deploy:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
with:
submodules: true # Fetch Hugo themes (true OR recursive)
fetch-depth: 0 # Fetch all history for .GitInfo and .Lastmod
- name: Setup Hugo
uses: peaceiris/actions-hugo@v2
with:
hugo-version: 'latest'
# extended: true # if use extended version
- name: Build
run: hugo
- name: Deploy
uses: peaceiris/actions-gh-pages@v3
with:
deploy_key: ${{ secrets.ACTIONS_DEPLOY_KEY }}
external_repository: &amp;lt;name&amp;gt;/&amp;lt;repository&amp;gt;
publish_dir: ./docs
publish_branch: master
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;timeout-minutes&lt;/code&gt;を設定しておくと、何らかの理由でビルドに失敗した場合でもGitHub Actionsのquotaを大量に消費せずに済みます。&lt;/li&gt;
&lt;li&gt;peaceiris/actions-gh-pagesのパラメータは以下の通り指定します。
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;external_repository&lt;/code&gt;: Hugoのビルドの生成物をpushするリポジトリ（最初の構成図ではsuzuna/blog）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;publish_dir&lt;/code&gt;: &lt;code&gt;external_repository&lt;/code&gt;にpushするディレクトリ
&lt;ul&gt;
&lt;li&gt;HugoのThemeのconfig.ymlの&lt;code&gt;publishDir&lt;/code&gt;にします&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;publish_branch&lt;/code&gt;: &lt;code&gt;external_repository&lt;/code&gt;のpushするブランチ&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;なお、&lt;code&gt;personal_token&lt;/code&gt;を用いる場合は、こちらを&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-yaml"&gt;deploy_key: ${{ secrets.ACTIONS_DEPLOY_KEY }}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;以下に変更します。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-yaml"&gt;personal_token: ${{ secrets.PERSONAL_TOKEN }}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;以上、GitHub Actionsとリポジトリ分割で快適なHugo環境を作れました！&lt;/p&gt;
&lt;h2 id="独自ドメイン対応"&gt;独自ドメイン対応&lt;/h2&gt;
&lt;p&gt;GitHub Pagesに独自ドメインを使っている場合は、以下のどちらかを行います。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;ソース管理用のリポジトリにおいて、staticフォルダ内にCNAMEファイルを置く&lt;/li&gt;
&lt;li&gt;以下のように、ソース管理用のリポジトリの&amp;quot;.github/workflows/main.yml&amp;quot;において、peaceiris/actions-gh-pages@v3のwith内にcnameを指定する
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/peaceiris/actions-gh-pages" target="_blank" rel="noopener noreferrer"&gt;peaceiris/actions-gh-pages&lt;/a&gt;のREADME.mdの&amp;quot;Add CNAME file cname&amp;quot;を参照&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class="language-yaml"&gt;- name: Deploy
uses: peaceiris/actions-gh-pages@v3
with:
deploy_key: ${{ secrets.ACTIONS_DEPLOY_KEY }}
external_repository: &amp;lt;name&amp;gt;/&amp;lt;repository&amp;gt;
publish_dir: ./docs
publish_branch: master
cname: &amp;lt;customdomain&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id="参考"&gt;参考&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;公式ドキュメント
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.github.com/ja/authentication/connecting-to-github-with-ssh/generating-a-new-ssh-key-and-adding-it-to-the-ssh-agent" target="_blank" rel="noopener noreferrer"&gt;新しい SSH キーを生成して ssh-agent に追加する&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/actions/checkout" target="_blank" rel="noopener noreferrer"&gt;actions/checkout&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/peaceiris/actions-gh-pages" target="_blank" rel="noopener noreferrer"&gt;peaceiris/actions-gh-pages&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;参考にさせていただいた記事
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://zenn.dev/bryutus/articles/hugo-github-pages-actions" target="_blank" rel="noopener noreferrer"&gt;Hugo + GitHub Pages / Actionsでブログを公開する&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://zenn.dev/nikaera/articles/hugo-github-actions-for-github-pages" target="_blank" rel="noopener noreferrer"&gt;Hugo + GitHub Pages + GitHub Actions で独自ドメインのウェブサイトを構築する&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>Rの便利なlogging package: logger</title><link>https://suzunano.net/posts/r-logger/</link><pubDate>Tue, 03 Oct 2023 00:00:00 +0900</pubDate><guid>https://suzunano.net/posts/r-logger/</guid><description>&lt;p&gt;Rでログを取る場合どうしていますか？インタラクティブなデータ分析なら&lt;code&gt;print&lt;/code&gt;や&lt;code&gt;cat&lt;/code&gt;でもよいのですが、バッチ処理を行うような場合は、&lt;code&gt;print&lt;/code&gt;文に現在時刻を含んだり、コンソールだけではなくファイルにも出力したりしたいところです。&lt;/p&gt;
&lt;p&gt;Pythonなら標準ライブラリのloggingやサードバーティーの&lt;a href="https://github.com/Delgan/loguru" target="_blank" rel="noopener noreferrer"&gt;loguru&lt;/a&gt;などがありますが、同じようにRでもloggerを使いたいですね。&lt;/p&gt;
&lt;p&gt;便利なロガーパッケージの&lt;a href="https://github.com/daroczig/logger" target="_blank" rel="noopener noreferrer"&gt;logger&lt;/a&gt;を紹介します。Pythonのloggingに近いインタフェースをしているので&lt;sup id="fnref:1"&gt;&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref"&gt;1&lt;/a&gt;&lt;/sup&gt;、loggingを使ったことがあればすんなり使えるのではないかと思います。&lt;/p&gt;
&lt;h2 id="環境"&gt;環境&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;R 4.3.1&lt;/li&gt;
&lt;li&gt;logger 0.2.2&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="インストール"&gt;インストール&lt;/h2&gt;
&lt;p&gt;インストールはこちらです。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-r"&gt;# CRANからインストールする場合
install.packages(&amp;quot;logger&amp;quot;)
# GitHubからインストールする場合
remotes::install_github(&amp;quot;daroczig/logger&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class="language-r"&gt;library(logger)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id="基本的な使い方"&gt;基本的な使い方&lt;/h2&gt;
&lt;p&gt;コンソールに表示するだけであれば、以下で使うことができます。最初に&lt;code&gt;logger::log_threshold&lt;/code&gt;でログを出力する閾値を定めます。&lt;/p&gt;
&lt;p&gt;logのlevelは高い順に&lt;code&gt;fatal &amp;gt; error &amp;gt; warn &amp;gt; success &amp;gt; info &amp;gt; debug &amp;gt; trace&lt;/code&gt;です。（&lt;a href="https://daroczig.github.io/logger/reference/log_levels.html" target="_blank" rel="noopener noreferrer"&gt;参考&lt;/a&gt;）&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-r"&gt;log_threshold(INFO)
log_info(&amp;quot;info&amp;quot;)
#&amp;gt; INFO [2023-10-04 02:38:37] info
log_warn(&amp;quot;warn&amp;quot;)
#&amp;gt; WARN [2023-10-04 02:38:37] warn
# DEBUGはINFOより下のlevelなので表示されない
log_debug(&amp;quot;debug&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;デフォルトでは&lt;code&gt;logger::log_formatter(logger::formatter_glue)&lt;/code&gt;が設定されているため、&lt;code&gt;glue::glue&lt;/code&gt;を使わなくても&lt;code&gt;glue&lt;/code&gt;の記法で変数を展開したり関数を評価したりできます。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-r"&gt;x &amp;lt;- 1
log_info(&amp;quot;x is {x}&amp;quot;)
#&amp;gt; INFO [2023-10-04 02:38:37] x is 1
log_info(&amp;quot;x + 1 is {x + 1}&amp;quot;)
#&amp;gt; INFO [2023-10-04 02:38:37] x + 1 is 2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;ログの出力先は、&lt;code&gt;logger::log_appender&lt;/code&gt;の引数&lt;code&gt;appender&lt;/code&gt;で設定することができます。デフォルトでは&lt;code&gt;logger::log_appender(appender=logger::appender_console)&lt;/code&gt;が設定されていますので、デフォルト設定のままコンソールに出力するだけであれば&lt;code&gt;logger::log_appender&lt;/code&gt;は不要です。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;logger::log_appender&lt;/code&gt;の引数&lt;code&gt;appender&lt;/code&gt;に&lt;code&gt;appender_&lt;/code&gt;で始まる関数を渡すことで、出力先を変えることができます。&lt;/p&gt;
&lt;p&gt;以下のようにすれば、コンソールとファイルに同時に出力することもできます&lt;sup id="fnref:2"&gt;&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref"&gt;2&lt;/a&gt;&lt;/sup&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-r"&gt;file_path &amp;lt;- tempfile()
log_appender(appender_console)
log_appender(appender_file(file=file_path))
log_threshold(INFO)
log_info(&amp;quot;info&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;appender_&lt;/code&gt;で始まる出力先を設定する関数は他にも色々ありますが、&lt;code&gt;logger::appender_slack&lt;/code&gt;で出力先をSlackにすることができるのが嬉しいポイントですね。内部では&lt;code&gt;slackr::slackr_msg&lt;/code&gt;を呼んでいるようです。バッチ処理が正常に終了したとき、あるいはエラーが出たときだけSlackに通知したいということがよくあります。&lt;/p&gt;
&lt;h2 id="出力先ごとに出し分ける"&gt;出力先ごとに出し分ける&lt;/h2&gt;
&lt;p&gt;loggerパッケージには&lt;code&gt;namespace&lt;/code&gt;と&lt;code&gt;index&lt;/code&gt;という概念があります。前者はloggerの名前空間（&lt;code&gt;logger::log_appender&lt;/code&gt;のデフォルト値は&lt;code&gt;&amp;quot;global&amp;quot;&lt;/code&gt;）、後者は同一のnamespaceの中でのloggerのindexです（デフォルト値は&lt;code&gt;1&lt;/code&gt;）。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;namespace&lt;/code&gt;は関数内でのログをメイン処理から分けるような場合などに使います。&lt;code&gt;index&lt;/code&gt;を活用すると、以下のようにコンソールとファイルで別々のlog
levelの閾値を設定するようなこともできます。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-r"&gt;file_path &amp;lt;- tempfile()
log_appender(appender_console, namespace=&amp;quot;global&amp;quot;, index=1)
log_appender(appender_file(file=file_path), namespace=&amp;quot;global&amp;quot;, index=2)
# コンソールにはINFO以上、ファイルにはDEBUG以上でログを出力する
log_threshold(INFO, namespace=&amp;quot;global&amp;quot;, index=1)
log_threshold(DEBUG, namespace=&amp;quot;global&amp;quot;, index=2)
log_info(&amp;quot;info&amp;quot;)
log_debug(&amp;quot;debug&amp;quot;)
log_trace(&amp;quot;trace&amp;quot;)
readLines(file_path)
#&amp;gt; [1] &amp;quot;INFO [2023-10-04 02:38:37] info&amp;quot; &amp;quot;DEBUG [2023-10-04 02:38:37] debug&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;なお、同一の&lt;code&gt;namespace&lt;/code&gt;と&lt;code&gt;index&lt;/code&gt;に&lt;code&gt;logger::appender_console&lt;/code&gt;のコンソール出力と&lt;code&gt;logger::appender_file&lt;/code&gt;のファイル出力を行う場合は、&lt;code&gt;logger::appender_tee&lt;/code&gt;というエイリアスが使えます。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-r"&gt;log_appender(appender_tee(file=file_path), namespace=&amp;quot;global&amp;quot;, index=1)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id="ログフォーマットをカスタマイズする"&gt;ログフォーマットをカスタマイズする&lt;/h2&gt;
&lt;p&gt;logのlayoutを&lt;code&gt;logger::log_layout&lt;/code&gt;の引数&lt;code&gt;layout&lt;/code&gt;に指定すればOKです。&lt;/p&gt;
&lt;p&gt;例えば、&lt;code&gt;logger::layout_json&lt;/code&gt;でjsonの形式にできます。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-r"&gt;file_path &amp;lt;- tempfile()
log_appender(appender_console)
log_threshold(INFO)
log_layout(layout_json())
log_info(&amp;quot;info&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;フォーマットをカスタマイズすることもできます。&lt;code&gt;logger::layout_glue_generator&lt;/code&gt;では、&lt;code&gt;glue&lt;/code&gt;の記法でカスタマイズすることができます。ログレベル（&lt;code&gt;level&lt;/code&gt;）や時刻（&lt;code&gt;time&lt;/code&gt;）、メッセージ（&lt;code&gt;msg&lt;/code&gt;）などいくつかの変数が組み込まれています。変数の一覧は&lt;a href="https://daroczig.github.io/logger/reference/get_logger_meta_variables.html" target="_blank" rel="noopener noreferrer"&gt;こちら&lt;/a&gt;にあります。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-r"&gt;file_path &amp;lt;- tempfile()
log_appender(appender_file(file=file_path))
log_threshold(INFO)
my_layout &amp;lt;- layout_glue_generator(
format=&amp;quot;{level} | {format(time, '%Y-%m-%dT%H:%M:%S+09:00')} | {msg}&amp;quot;
)
log_layout(my_layout)
log_info(&amp;quot;info&amp;quot;)
readLines(file_path)
#&amp;gt; [1] &amp;quot;INFO | 2023-10-04T02:38:37+09:00 | info&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id="おわりに"&gt;おわりに&lt;/h2&gt;
&lt;p&gt;これくらいを抑えておけばひとまず使えるかと思います。&lt;a href="https://daroczig.github.io/logger/index.html" target="_blank" rel="noopener noreferrer"&gt;vignette&lt;/a&gt;が充実しているので詳細はvignetteをご覧ください。&lt;/p&gt;
&lt;h2 id="reference"&gt;Reference&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/daroczig/logger" target="_blank" rel="noopener noreferrer"&gt;GitHub - daroczig/logger&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://daroczig.github.io/logger/index.html" target="_blank" rel="noopener noreferrer"&gt;vignette&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="footnotes" role="doc-endnotes"&gt;
&lt;hr&gt;
&lt;ol&gt;
&lt;li id="fn:1"&gt;
&lt;p&gt;パッケージの作者もそのように書いています。（&lt;a href="https://daroczig.github.io/logger/index.html" target="_blank" rel="noopener noreferrer"&gt;参考&lt;/a&gt;）“A
lightweight, modern and flexibly logging utility for R – heavily
inspired by the futile.logger R package and logging Python module.”&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:2"&gt;
&lt;p&gt;なお、RMarkdownやQuartoのコードブロックにログを出力する場合、&lt;code&gt;logger::appender_console&lt;/code&gt;や&lt;code&gt;logger::appender_stderr&lt;/code&gt;では出力が表示されないようです。&lt;code&gt;logger::appender_stdout&lt;/code&gt;では出力できます。環境はrmarkdown:
2.25, knitr: 1.44, Quarto (CLI): 1.3.450 (Windows 10, 64bit)です。&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</description></item><item><title>Typora + Googleドライブで快適なMarkdown環境を作った</title><link>https://suzunano.net/posts/typora-google-drive/</link><pubDate>Wed, 27 Sep 2023 00:00:00 +0900</pubDate><guid>https://suzunano.net/posts/typora-google-drive/</guid><description>&lt;h2 id="要約"&gt;要約&lt;/h2&gt;
&lt;p&gt;Markdownでメモを作成して複数のPCで同期したい場合、MarkdownエディタとしてTyporaを使い、PC版Googleドライブをインストールしてメモの保存先をGoogleドライブにしてオフラインで使用可能にするのをおすすめの方法の一つとして推します。&lt;/p&gt;
&lt;p&gt;他の人のMarkdownエディタ事情を読むのが好きなので自分も書いてみました。&lt;/p&gt;
&lt;h2 id="どういうこと"&gt;どういうこと？&lt;/h2&gt;
&lt;h3 id="この記事の対象"&gt;この記事の対象&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;PCでMarkdownのメモを作成する人&lt;/li&gt;
&lt;li&gt;複数のPCでメモを同期させたい人
&lt;ul&gt;
&lt;li&gt;特に、PCがインターネットに繋がっていない環境でもファイルを開いたり編集したりしたい人（ノートPCを持ち運ぶなど）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="やり方"&gt;やり方&lt;/h3&gt;
&lt;p&gt;以下を満たせればよいわけです。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;何らかのMarkdownエディタを用いて&lt;/li&gt;
&lt;li&gt;Markdownの保存先として何らかのオンラインストレージに保存する
&lt;ul&gt;
&lt;li&gt;可能であれば、オフラインにもデータを持っておき、オンライン時に同期できるようにする&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;1個目の要件については好きなMarkdownエディタを使えばよいのですが、個人的には&lt;a href="https://typora.io/" target="_blank" rel="noopener noreferrer"&gt;Typora&lt;/a&gt;がよかったです。（2023/9/26時点では有料、3つのデバイスまで使えて買い切り14.99ドル）&lt;/p&gt;
&lt;p&gt;個人的にメモ用のMarkdownエディタに求めることとしてはこの辺です。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Markdownのソースコードのエディタも、リアルタイムプレビュー（Markdownのソースコードを入力すると、画面上ではリアルタイムでレンダリングされた状態が表示されている）も使えること&lt;/li&gt;
&lt;li&gt;左ペインにフォルダ内のMarkdownファイルを一覧表示するエクスプローラが付いていること&lt;/li&gt;
&lt;li&gt;エディタ起動時にそのエクスプローラで指定したフォルダが開かれていること&lt;/li&gt;
&lt;li&gt;指定したフォルダ内のMarkdownファイルを対象にしたgrepができること&lt;/li&gt;
&lt;li&gt;指定した行数・列数のTableをGUIで挿入できること&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;ブログや技術記事はがっつり書きたいので、VSCodeでエディタペインとプレビューペインの2ペインを並べて書いていますが&lt;sup id="fnref:1"&gt;&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref"&gt;1&lt;/a&gt;&lt;/sup&gt;、メモは気軽に取りたいのでリアルタイムプレビューでWYSIWIGでも書きたいものです。また、メモをメモフォルダ内に雑多に突っ込んでいまして、エディタ起動時にエクスプローラ上でフォルダが開かれていてgrepが使いたいです。Tableを手で組むのは辛いのでこれもサポートしていてほしいです。&lt;/p&gt;
&lt;p&gt;Typoraはこれらを満たしています。有料化前から使っていて気に入っていたので有料化した際にライセンス代を支払いましたが、最近だと無料でも似たような機能を持つエディタがあるので好きなのを使えばいいと思います。無料でオープンソースの&lt;a href="https://www.marktext.cc/" target="_blank" rel="noopener noreferrer"&gt;MarkText&lt;/a&gt;なんかもよさそうですね。（前はGitHubでしか配布していなかった気がしますが今見たらインストーラーがあった）&lt;/p&gt;
&lt;p&gt;保存先としてはオンラインストレージですと複数のPCで同期されてよいですね。PCが壊れても安心です。移動中などインターネットが繋がらない場所でも開いたり編集したりしたい場合は、ファイルをローカルのストレージにも持っておき、インターネットに繋がっていたらリアルタイムで同期するというオフラインモードでも開けるサービスがよいです。&lt;/p&gt;
&lt;p&gt;PC版Googleドライブはこの辺の要件を満たします。インストールするとローカルファイルと同じようにファイルパスを持つようになります。WindowsではGoogleドライブにドライブレターが一つ割り当てられて&lt;code&gt;D:\マイドライブ&lt;/code&gt;のように、Macでは&lt;code&gt;/Users/&amp;lt;ユーザネーム&amp;gt;/&amp;lt;Googleアカウント名&amp;gt;/&lt;/code&gt;のように扱えます。これが中々便利でして、Typoraの保存先として&lt;code&gt;D:\マイドライブ\メモフォルダ&lt;/code&gt;を指定しておき、このフォルダをオフラインでも使用できるモードにしておけば&lt;sup id="fnref:2"&gt;&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref"&gt;2&lt;/a&gt;&lt;/sup&gt;全ての要件を満たしてくれます。MicrosoftのOneDriveとかでもいいですが。&lt;/p&gt;
&lt;div class="footnotes" role="doc-endnotes"&gt;
&lt;hr&gt;
&lt;ol&gt;
&lt;li id="fn:1"&gt;
&lt;p&gt;余談ですが、VSCodeではMarkdown All in One （色々）+ markdownlint （文法チェック）+ Markdown PDF（PDF出力） + Table Formatter（Tableの入力支援）の4つの拡張機能がお気に入りです。&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:2"&gt;
&lt;p&gt;Windowsならエクスプローラ、MacならFinderで指定のフォルダを右クリックして出てくるメニューで設定できます。&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</description></item><item><title>ニコニコ動画の再生数の推移を見られるWebアプリを作った</title><link>https://suzunano.net/posts/nicolog/</link><pubDate>Thu, 21 Sep 2023 00:00:00 +0900</pubDate><guid>https://suzunano.net/posts/nicolog/</guid><description>&lt;h2 id="概要"&gt;概要&lt;/h2&gt;
&lt;p&gt;ニコニコ動画の動画について、再生数、マイリスト数、コメント数、いいね数の日次の推移を表示するWebアプリを作りました。&lt;/p&gt;
&lt;p&gt;こんな感じで、動画のIDを入力すると過去の値を表示します。対象は再生数が3000以上の動画（2023/9/19時点で410万件程度）です。&lt;/p&gt;
&lt;img src="./screenshot.png" width="600px"&gt;
&lt;h2 id="技術構成"&gt;技術構成&lt;/h2&gt;
&lt;p&gt;当日の断面における個々の動画の再生数などのメタデータを返す&lt;a href="https://site.nicovideo.jp/search-api-docs/snapshot" target="_blank" rel="noopener noreferrer"&gt;スナップショット検索API v2&lt;/a&gt;というAPIをニコニコ動画が公開しています。このAPIは当日分のデータしか返さないため、毎日リクエストしてデータを蓄積しています。&lt;/p&gt;
&lt;p&gt;面白そうなデータなので毎日貯めているのですが、このような過去の再生数を表示するWebサイトはほとんどない&lt;sup id="fnref:1"&gt;&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref"&gt;1&lt;/a&gt;&lt;/sup&gt;こともあり、勉強も兼ねてデータ基盤とWebアプリを作りました。&lt;/p&gt;
&lt;p&gt;構成図はこちらです。&lt;/p&gt;
&lt;img src="./architecture.png" width="800px"&gt;
&lt;ul&gt;
&lt;li&gt;バックエンド
&lt;ul&gt;
&lt;li&gt;クローリング部分 (VPS, Debian)
&lt;ul&gt;
&lt;li&gt;cronで1日1回スナップショットAPIをリクエストして結果のCSVを保存する (R)
&lt;ul&gt;
&lt;li&gt;ここだけRなのは、過去にRで書いたコードを流用したから&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;CSVファイルをCloud Storageにアップロードする (Python)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;データ基盤部分 (Google Cloud)
&lt;ul&gt;
&lt;li&gt;CSVファイルがCloud StorageにアップロードされたらBigQueryに書き込むCloud Functions (Python)&lt;/li&gt;
&lt;li&gt;過去の再生数などを保持するBigQuery&lt;/li&gt;
&lt;li&gt;動画IDをクエリストリングに与えるとBigQueryをクエリして過去の再生数などを返すCloud Functions (Python)
&lt;ul&gt;
&lt;li&gt;SQLインジェクション対策のため、フロントエンドだけでなくバックエンドでも入力値をバリデーション&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;フロントエンド (Google Cloud)
&lt;ul&gt;
&lt;li&gt;Cloud Run (Streamlit)
&lt;ul&gt;
&lt;li&gt;Artifact RegistryにpushしたDocker imageでdeploy&lt;/li&gt;
&lt;li&gt;以前はHerokuにdeployしていたが、Heroku代が高いのでCloud Runに引っ越した
&lt;ul&gt;
&lt;li&gt;バックエンドとフロントエンドをGoogle Cloudに揃えたことにより、バックエンドのCloud Functionsをリクエストする認証周りがすっきりするメリットもあった&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;VPS部分のコードは、GitHub Actionsを使って、mainブランチにpushするとVPSにデプロイ（git pull）しています。Google Cloud部分のコードはTerraformで定義しています。&lt;/p&gt;
&lt;p&gt;クローリング・スクレイピングはVPS (cron)、データ基盤はBigQuery + Terraformというのは気に入っている構成でよく採用しています。&lt;/p&gt;
&lt;p&gt;前者にVPSを利用するのは、長時間のクローリングでも気にせずコードを動かせることと、アウトバウンドの通信量に課金がされないことからです。Cloud FunctionsやCloud Runは実行時間に上限があるので、引っかかる場合は処理をうまく分割してあげる必要がありますが、VPSならcronで雑にスクリプトを動かせます。&lt;/p&gt;
&lt;p&gt;なお、VPSのディスク容量などの監視ツールとして、はてな製のサービスである&lt;a href="https://ja.mackerel.io/" target="_blank" rel="noopener noreferrer"&gt;Mackerel&lt;/a&gt;を使っています。無料プランでは過去のメトリクスが1日分しか見られませんが、トリガーに引っかかったときはメールやSlackで通知でき、個人開発の心強い味方です。&lt;/p&gt;
&lt;h2 id="技術的なtips"&gt;技術的なTips&lt;/h2&gt;
&lt;h3 id="bigqueryのテーブル設計-パーティショニング"&gt;BigQueryのテーブル設計: パーティショニング&lt;/h3&gt;
&lt;p&gt;BigQueryのテーブルを一部抜粋します。カラムは順に日時、動画ID、再生数を表します。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;lastModified&lt;/th&gt;
&lt;th&gt;contentId&lt;/th&gt;
&lt;th&gt;viewCounter&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;2023-09-15T08:59:37+09:00&lt;/td&gt;
&lt;td&gt;sm1097445&lt;/td&gt;
&lt;td&gt;16575782&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2023-09-16T08:53:20+09:00&lt;/td&gt;
&lt;td&gt;sm1097445&lt;/td&gt;
&lt;td&gt;16576909&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2023-09-17T08:52:41+09:00&lt;/td&gt;
&lt;td&gt;sm1097445&lt;/td&gt;
&lt;td&gt;16578086&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;フロント側でデータを取得する際、例えば取得したい動画IDをsm1097445とすると、以下のクエリを書くことになります。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-SQL"&gt;SELECT lastModified, contentId, viewCounter
FROM TABLE_NAME
WHERE contentId = &amp;quot;sm1097445&amp;quot;
ORDER BY lastModified;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;クエリ量を削減するためにこのテーブルにパーティショニングを設定します。where句で絞るcontentIdでパーティショニングしたいところですが、パーティショニング可能なのは整数範囲、時間単位、取り込み時間のいずれかです。&lt;/p&gt;
&lt;p&gt;contentIdはアルファベットの小文字2文字+数字1文字以上で表されることを利用し、contentIdの数字部分を4000で割った余りであるidModという列をテーブルにwrite_appendする際に付け加え、この列でパーティショニングすることにしました。4000というのは、当時BigQueryの整数範囲パーティショニングの上限は4000個までだったからです。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;lastModified&lt;/th&gt;
&lt;th&gt;contentId&lt;/th&gt;
&lt;th&gt;viewCounter&lt;/th&gt;
&lt;th&gt;idMod&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;2023-09-15T08:59:37+09:00&lt;/td&gt;
&lt;td&gt;sm1097445&lt;/td&gt;
&lt;td&gt;16575782&lt;/td&gt;
&lt;td&gt;1445&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2023-09-16T08:53:20+09:00&lt;/td&gt;
&lt;td&gt;sm1097445&lt;/td&gt;
&lt;td&gt;16576909&lt;/td&gt;
&lt;td&gt;1445&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2023-09-17T08:52:41+09:00&lt;/td&gt;
&lt;td&gt;sm1097445&lt;/td&gt;
&lt;td&gt;16578086&lt;/td&gt;
&lt;td&gt;1445&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;SQLでパーティショニングのidMod列をwhere句に含めることで、理想的にはクエリサイズが1/4000に抑えられます。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-SQL"&gt;SELECT lastModified, contentId, viewCounter
FROM TABLE_NAME
WHERE contentId = &amp;quot;sm1097445&amp;quot; and idMod = 1445
ORDER BY lastModified;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;今回はcontentIdでフィルタするクエリを書くためにこのようなパーティショニングを設定しましたが、例えば同一のlastModifiedにおけるレコードを全件取得するような使い方をするならlastModified列で時間単位パーティショニングすることになります。&lt;/p&gt;
&lt;h3 id="terraformの環境分け本番環境と開発環境"&gt;Terraformの環境分け（本番環境と開発環境）&lt;/h3&gt;
&lt;p&gt;Google Cloud部分は、本番環境（prod）と開発環境（dev）を分けられるように定義しています。&lt;/p&gt;
&lt;p&gt;Terraformで異なる環境を作成する方法としては以下の三つがメジャーなところかと思いますが、三番目の方法を取っています。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Terraform Workspacesを使う&lt;/li&gt;
&lt;li&gt;moduleを使う&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.tfbackend&lt;/code&gt;ファイルと&lt;code&gt;.tfvars&lt;/code&gt;ファイルを用いて変数で環境を分ける&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;具体的にはこちらです。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;リソース名の先頭に環境名を付ける（例: &lt;code&gt;prod-hoge-bucket&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;環境名を&lt;code&gt;prod.tfvars&lt;/code&gt;, &lt;code&gt;dev.tfvars&lt;/code&gt;に記載する&lt;/li&gt;
&lt;li&gt;Terraformのstatusを管理するCloud Storageの情報を&lt;code&gt;prod.tfbackend&lt;/code&gt;, &lt;code&gt;dev.tfbackend&lt;/code&gt;に記載する&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;小規模のバックエンド基盤では楽な方法ですね。&lt;/p&gt;
&lt;p&gt;詳細には、こちらの記事（&lt;a href="https://zenn.dev/smartround_dev/articles/5e20fa7223f0fd" target="_blank" rel="noopener noreferrer"&gt;Terraformでmoduleを使わずに複数環境を構築する&lt;/a&gt;）が丁寧に解説されています。&lt;/p&gt;
&lt;details&gt;
&lt;summary&gt;もう少し説明&lt;/summary&gt;
&lt;p&gt;ディレクトリ構成はこのような感じです。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-text"&gt;.
└── terraform
├── envs
│ ├── dev
│ │ ├── dev.tfbackend
│ │ └── dev.tfvars
│ └── prod
│ ├── prod.tfbackend
│ └── prod.tfvars
├── main.tf
└── variables.tf
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;main.tf&lt;/code&gt;は以下の通り&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-text"&gt;provider &amp;quot;google&amp;quot; {
project = var.project_id
region = var.project_region
}
terraform {
# バージョンは任意
required_version = &amp;quot;~&amp;gt; 1.5.5&amp;quot;
required_providers {
google = {
source = &amp;quot;hashicorp/google&amp;quot;
version = &amp;quot;~&amp;gt; 4.80.0&amp;quot;
}
archive = {
source = &amp;quot;hashicorp/archive&amp;quot;
version = &amp;quot;~&amp;gt; 2.4.0&amp;quot;
}
}
backend &amp;quot;gcs&amp;quot; {
# envs/(env_name)/(env_name).tfbackendに定義
}
}
# 例えばCloud Storageのバケットを作成してみる
resource &amp;quot;google_storage_bucket&amp;quot; &amp;quot;tmp_bucket&amp;quot; {
name = &amp;quot;${var.env}-tmp-bucket&amp;quot;
location = var.project_region
force_destroy = var.env == &amp;quot;prod&amp;quot; ? true : false
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;envs/dev/dev.tfvars&lt;/code&gt;は以下の通り&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-text"&gt;env = &amp;quot;dev&amp;quot;
project_region = &amp;quot;&amp;lt;PROJECT_REGION&amp;gt;&amp;quot;
project_id = &amp;quot;&amp;lt;PROJECT_ID&amp;gt;&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;envs/dev/dev.tfbackend&lt;/code&gt;（stateを置くバックエンドのCloud Storageの情報）は以下の通り&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-text"&gt;bucket = &amp;quot;&amp;lt;BUCKET_NAME&amp;gt;&amp;quot;
prefix = &amp;quot;&amp;lt;PREFIX&amp;gt;&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;variables.tf&lt;/code&gt;は以下の通り&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-text"&gt;variable &amp;quot;env&amp;quot; {
type = string
description = &amp;quot;environment name&amp;quot;
}
variable &amp;quot;project_region&amp;quot; {
type = string
description = &amp;quot;Google Cloud Region&amp;quot;
}
variable &amp;quot;project_id&amp;quot; {
type = string
description = &amp;quot;Project ID&amp;quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;以上のように用意して、&lt;code&gt;terraform init&lt;/code&gt;するときにtfbackendファイルを、&lt;code&gt;terraform plan&lt;/code&gt;と&lt;code&gt;terraform apply&lt;/code&gt;するときにtfvarsファイルをオプションで渡すことで、環境ごとに異なるバックエンドを参照して異なるリソースを作成することができます。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;# dev環境にdeployする
$ cd terraform
$ terraform init -backend-config=envs/dev/dev.tfbackend
$ terraform plan -var-file=envs/dev/dev.tfvars
$ terraform apply -var-file=envs/dev/dev.tfvars
&lt;/code&gt;&lt;/pre&gt;
&lt;/details&gt;
&lt;div class="footnotes" role="doc-endnotes"&gt;
&lt;hr&gt;
&lt;ol&gt;
&lt;li id="fn:1"&gt;
&lt;p&gt;調べた範囲だと&lt;a href="https://www.nicolog.jp/" target="_blank" rel="noopener noreferrer"&gt;ニコログ&lt;/a&gt;があります。こちらはランキングに載った動画や新着動画は収集されており、該当の動画だと1時間単位でデータがありますが、該当しないと収集されていないようです。&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</description></item><item><title>radikoでラジオ番組をタイムフリー録音してGoogleドライブにアップロードする</title><link>https://suzunano.net/posts/radiko-recording/</link><pubDate>Sun, 14 May 2023 00:00:00 +0900</pubDate><guid>https://suzunano.net/posts/radiko-recording/</guid><description>&lt;h2 id="概要"&gt;概要&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://radiko.jp/" target="_blank" rel="noopener noreferrer"&gt;radiko&lt;/a&gt;の指定した番組の放送終了後にVPS上でタイムフリー録音をダウンロードし、Google Driveにアップロードするようにしました。（自分のみの私的利用を目的としています）&lt;/p&gt;
&lt;p&gt;わたしはラジオが好きでいつも作業したりコードを書いたりしながら聞いています。radikoのタイムフリー録音を使うと、1週間以内であれば放送終了後の番組を聞くことができるのですが、1週間以上経っても後から聞き返したくなることがあります。&lt;/p&gt;
&lt;p&gt;番組をダウンロードするフリーソフトとしては、&lt;a href="https://dogaradi.com/dl-radirec/" target="_blank" rel="noopener noreferrer"&gt;らじれこ&lt;/a&gt;という優れたものがあります。これを活用して、1週間に1回、その週の番組をまとめてダウンロードしていました。しかし、聞く番組が増えてくると手でダウンロードボタンを押すのが面倒になってきますし、ダウンロードをし忘れることもあります。&lt;/p&gt;
&lt;p&gt;技術屋としては技術で解決したいところです。色々調べてみると、タイムフリー録音するスクリプトを先人が作ってくれていましたので、それを活用して自動で録音する仕組みを作りました。&lt;/p&gt;
&lt;p&gt;仕組みとしては以下のようになっています。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;タイムフリー録音をダウンロードするスクリプトをVPSにおいてcronで実行
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/uru2/rec_radiko_ts" target="_blank" rel="noopener noreferrer"&gt;uru2/rec_radiko_ts: Radiko timefree program recorder&lt;/a&gt;をベースに、引数の与え方を少し変えたかったのでラッパーのPythonスクリプトを作成&lt;/li&gt;
&lt;li&gt;放送終了の5分後に、その番組を保存するようなcronを番組の数だけ書く&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;録音したファイルをGoogleドライブにアップロードし、アップロード完了後にVPS上からファイルを削除するPythonスクリプトをVPS上でcronで実行
&lt;ul&gt;
&lt;li&gt;Pythonで自作&lt;/li&gt;
&lt;li&gt;参考
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://servercan.net/blog/2022/03/pydrive%E3%81%AB%E3%82%88%E3%82%8Bgoogle%E3%83%89%E3%83%A9%E3%82%A4%E3%83%96%E3%81%B8%E3%81%AE%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%82%A2%E3%83%83%E3%83%97%E3%83%AD%E3%83%BC%E3%83%89/" target="_blank" rel="noopener noreferrer"&gt;PyDriveによるGoogleドライブへのファイルアップロード - Fun Scripting 2.0&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://note.nkmk.me/python-pydrive-download-upload-delete/" target="_blank" rel="noopener noreferrer"&gt;Python, PyDriveでGoogle Driveのダウンロード、アップロード、削除など | note.nkmk.me&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;GoogleドライブにネットワークドライブのようにエクスプローラからアクセスできるGoogle公式のツールをWindows PCにインストール
&lt;ul&gt;
&lt;li&gt;ローカルに保存しているのと同じような感覚で快適にアクセスできる&lt;/li&gt;
&lt;li&gt;参考
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://atmarkit.itmedia.co.jp/ait/articles/2104/12/news019.html" target="_blank" rel="noopener noreferrer"&gt;GoogleドライブをG:などに割り当ててWindows 10でシームレスに使う：Tech TIPS - ＠IT&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;1個目と2個目については、エラーが発生した場合はtry～exceptでつかまえてSlackにwebhookで通知しています。&lt;/p&gt;
&lt;p&gt;技術選定の理由はこんな感じです。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;VPS
&lt;ul&gt;
&lt;li&gt;radikoへアクセスするアウトバウンドの通信がそれなりにあるので、通信量に課金がされるGCPなどのクラウドではなく、元々借りていた通信量に課金がされないVPSを利用。
&lt;ul&gt;
&lt;li&gt;とりあえずcronで実行できるので楽ですね。cronが取っ散らかっていく問題はありますが…&lt;/li&gt;
&lt;li&gt;VPSへのコードデプロイはGitHubのmainブランチにpushしたらVPS上でコードをpullするGitHub Actionsで行っています。&lt;/li&gt;
&lt;li&gt;ちなみにConoHa（東京リージョン）ではradikoのフリープランでも東京の番組が聞けます。XServer VPSではプレミアムプランでなければ聞くことができませんでした。（サーバがあるリージョンの問題っぽい）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Googleドライブ
&lt;ul&gt;
&lt;li&gt;Googleドライブからローカルへのダウンロードに通信量がかからず、選択した容量で月額決まった料金となる上に、先述の通り、GoogleドライブはWindowsからネットワークドライブのように扱えて便利なため。スマートフォンからもアプリでアクセス可能。
&lt;ul&gt;
&lt;li&gt;最初は何も考えずGCPのCloud Storageを使おうとしたのですが、このメリットを思い出して変更しました。技術選定大事。&lt;/li&gt;
&lt;li&gt;保存容量が15GBまでなら無料、200GBでも月380円で済みます。ちなみに1時間番組1本で20MB程度です。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="tips"&gt;Tips&lt;/h2&gt;
&lt;p&gt;以下、実装の過程で出会った技術的なTipsを書き留めます。&lt;/p&gt;
&lt;h3 id="pythonでのsubprocessrunのエラーハンドリング"&gt;Pythonでの&lt;code&gt;subprocess.run()&lt;/code&gt;のエラーハンドリング&lt;/h3&gt;
&lt;p&gt;Pythonからコマンドを実行するときに使う&lt;code&gt;subprocess.run()&lt;/code&gt;ですが、正常に実行されたときとエラーが起きた時で処理を分けて、エラーの場合はエラーメッセージを取得したいというケースがあります。&lt;/p&gt;
&lt;p&gt;解決策としては、引数に&lt;code&gt;capture_output=True&lt;/code&gt;と&lt;code&gt;text=True&lt;/code&gt;を指定します。前者により出力を受け取り、後者により出力をbyte型ではなく文字列で受け取ります。
リターンコード、標準出力、標準エラー出力はreturncode, stdout, stderrで受け取ることができます。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-python"&gt;# 任意のコマンド
cmd = &amp;quot;bash hoge.sh&amp;quot;
res = subprocess.run(cmd, shell=True, text=True, capture_output=True)
if res.returncode == 0:
logger.info(f&amp;quot;success | {res.stdout}&amp;quot;)
else:
logger.error(f&amp;quot;error | {res.stderr}&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;引数に&lt;code&gt;check=True&lt;/code&gt;を指定すると、returncodeが0ではないときに&lt;code&gt;subprocess.CalledProcessError&lt;/code&gt;の例外を起こすことができます。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-python"&gt;# 上の例と同じことができる
cmd = &amp;quot;bash hoge.sh&amp;quot;
try:
res = subprocess.run(cmd, shell=True, text=True, capture_output=True, check=True)
logger.info(res.stdout)
except subprocess.CalledProcessError as e:
logger.error(e)
logger.error(res.stderr)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id="pythonスクリプト中での相対パスを固定する"&gt;Pythonスクリプト中での相対パスを固定する&lt;/h3&gt;
&lt;p&gt;このようなディレクトリ構造において、以下のスクリプトをmain.pyで保存します。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-text"&gt;hoge
|-- fuga
|-- main.py
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class="language-python"&gt;import os
print(os.getcwd())
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;このスクリプトは、カレントディレクトリがhogeかfugaかで返ってくる値が異なります。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;hoge $ python ./fuga/main.py
hoge
hoge/fuga $ python main.py
hoge/fuga
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;これでは、コード中で相対パスでファイルを読み込んでいるとき（プロジェクトディレクトリであるfugaを起点にするようなパターン）、cronなどでシェルから実行する場合、カレントディレクトリによって挙動が変わり不便です。&lt;/p&gt;
&lt;p&gt;Python&amp;gt;=3.9では、以下のように&lt;code&gt;os.chdir(os.path.dirname(__file__))&lt;/code&gt;を足してあげることで、コードが存在するディレクトリを起点にそれより下のコードが実行されて便利です。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-python"&gt;import os
os.chdir(os.path.dirname(__file__))
print(os.getcwd())
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;参考：&lt;a href="https://note.nkmk.me/python-script-file-path/" target="_blank" rel="noopener noreferrer"&gt;Pythonで実行中のファイルの場所（パス）を取得する__file__ | note.nkmk.me&lt;/a&gt;&lt;/p&gt;</description></item><item><title>アニメのキャプチャ画像から線画を作る</title><link>https://suzunano.net/posts/line-drawing/</link><pubDate>Sat, 14 Jan 2023 00:00:00 +0900</pubDate><guid>https://suzunano.net/posts/line-drawing/</guid><description>&lt;h2 id="はじめに"&gt;はじめに&lt;/h2&gt;
&lt;p&gt;OpenCVを用いてアニメのキャプチャ画像から線画を生成してみました。鉛筆で書いたような味のある線画が作れました。&lt;/p&gt;
&lt;p&gt;線画生成は最近では深層学習の手法を使うものもありますが、この記事では古典的な画像処理の方法で作ってみました。アニメに限らずいわゆるアニメ風のイラストであれば記事の方法で同様に適用できると思います。&lt;/p&gt;
&lt;h2 id="線画抽出のロジック"&gt;線画抽出のロジック&lt;/h2&gt;
&lt;p&gt;イラストの線画抽出については、グレースケールで読み込み -&amp;gt; 1回収縮 -&amp;gt;
収縮前との差を取る -&amp;gt;
白黒反転という方法でそれなりに綺麗なものが作れることが知られています（例えば：&lt;a href="https://qiita.com/khsk/items/6cf4bae0166e4b12b942" target="_blank" rel="noopener noreferrer"&gt;そこそこな線画を目指す
OpenCV -
Qiita&lt;/a&gt;）。収縮の際のカーネルは4近傍（2x2の行列）か8近傍（3x3の行列）を用います。シンプルですが結構きれいに線画が抽出できる優れた方法です。&lt;/p&gt;
&lt;p&gt;この方法を出発点に、より綺麗に線画が抽出できる方法を探ってみました。&lt;/p&gt;
&lt;p&gt;結論としては、グレースケールで読み込み -&amp;gt; 適応的ヒストグラム平坦化 -&amp;gt;
1回収縮 -&amp;gt; 収縮前との差を取る -&amp;gt; 白黒反転 -&amp;gt; Non-local Means
Denoising -&amp;gt; ガンマ変換で綺麗な線画が作れました。パラメータなどは決め打ちしたのでもっといい方法はあると思います。&lt;/p&gt;
&lt;h2 id="環境"&gt;環境&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Windows 10&lt;/li&gt;
&lt;li&gt;python 3.10.0&lt;/li&gt;
&lt;li&gt;opencv-python 4.5.5.64&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="実装"&gt;実装&lt;/h2&gt;
&lt;p&gt;&lt;a href="http://kinmosa.com/" target="_blank" rel="noopener noreferrer"&gt;きんいろモザイク&lt;/a&gt;の第1期12話のこちらの画像から線画を作ってみます。&lt;/p&gt;
&lt;img src="./image/kinmosa.jpg" width="800px"&gt;
&lt;pre&gt;&lt;code class="language-python"&gt;import cv2
import numpy as np
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;画像を読み込みます。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-python"&gt;image_original = cv2.imread(&amp;quot;image_original/kinmosa.jpg&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;こちらがベースラインとなる「グレースケールで読み込み -&amp;gt; 1回収縮 -&amp;gt;
収縮前との差を取る -&amp;gt; 白黒反転」のロジックです。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-python"&gt;image = cv2.cvtColor(image_original, cv2.COLOR_BGR2GRAY)
image_dilate = cv2.dilate(image, np.ones((3, 3), np.uint8), iterations=1)
image = cv2.absdiff(image, image_dilate)
image = cv2.bitwise_not(image)
&lt;/code&gt;&lt;/pre&gt;
&lt;img src="./image/kinmosa_base.jpg" width="800px"&gt;
&lt;p&gt;この段階で十分きれいで驚きました。ただ、2つ改善したい点があります。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一番右の小路綾さんの左手と胴の間の線や、セーターとスカートの間の線、セーターのしわがうまく抽出できていません。
&lt;ul&gt;
&lt;li&gt;元の画像と見比べると分かりますが、これらはセーターの濃紺の中にある黒い線ですから、単純な収縮では線を取り出しづらいのかと思います。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;全体的に線が薄いため、線にメリハリを付けたいです。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;1点目はコントラストを平坦化するのがよさそうです。特に、画像の小さな領域ごとにヒストグラムを平坦化する適応的ヒストグラム平坦化（&lt;code&gt;cv2.createCLAHE&lt;/code&gt;）が効きそうな感じがします。というわけで、グレースケール化した後に適応的ヒストグラム平坦化をかけてみます。&lt;/p&gt;
&lt;p&gt;また、2点目については、ガンマ変換をかけることにします。&lt;/p&gt;
&lt;p&gt;ガンマ変換前の画像のnp.ndarrayを&lt;code&gt;x&lt;/code&gt;とするとき、単に&lt;code&gt;x/255**gamma*255&lt;/code&gt;でガンマ変換できますが、xの全ての画素についてこの計算をするのは計算負荷が大きいです。0から255までの整数値を&lt;code&gt;a&lt;/code&gt;としたときに、ガンマ変換後の値&lt;code&gt;y&lt;/code&gt;は&lt;code&gt;a/255**gamma*255&lt;/code&gt;で与えられますから、ガンマ変換を実装する上では、aとyのマッピングテーブルを用意しておき、このテーブルのaをxで読み替えるのが賢い方法です。&lt;/p&gt;
&lt;p&gt;なお、ガンマ変換をかけると線の周辺や真っ白の領域に黒いモスキートノイズが浮かび上がってしまうため、ガンマ変換の前に何らかのフィルターをかけてノイズを軽減する必要があります。このタイプのノイズを取るには、Bilateral
Filter（&lt;code&gt;cv2.bilateralFilter&lt;/code&gt;）、Adaptive Bilateral
Filter（cv2&amp;gt;=3.0.0で削除された）、Non-local Means
Denoising（&lt;code&gt;cv2.fastNlMeansDenoising&lt;/code&gt;）などがあると思います。ここではNon-local
Means Denoisingを試してみました。&lt;/p&gt;
&lt;p&gt;以上を実装してみます。なお、画像処理の各種パラメータは私が色々試してみて良さそうだと感じたものを適当に採用しています。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-python"&gt;def contrast_equalization(img: np.ndarray, clip_limit: float, tile_grid_size: int) -&amp;gt; np.ndarray:
clahe = cv2.createCLAHE(clipLimit=clip_limit, tileGridSize=(tile_grid_size, tile_grid_size))
res = clahe.apply(img)
return res
def gamma_transformation(img: np.ndarray, gamma: float) -&amp;gt; np.ndarray:
x = np.arange(0, 256)
look_up_table = (x / 255) ** gamma * 255
look_up_table = look_up_table.astype(np.uint8)
return cv2.LUT(img, look_up_table)
image = cv2.cvtColor(image_original, cv2.COLOR_BGR2GRAY)
# 追加
image = contrast_equalization(image, 2.0, 8)
image_dilate = cv2.dilate(image, np.ones((3, 3), np.uint8), iterations=1)
image = cv2.absdiff(image, image_dilate)
image = cv2.bitwise_not(image)
# 追加
image = cv2.fastNlMeansDenoising(image, h=3, templateWindowSize=7, searchWindowSize=21)
# 追加
image = gamma_transformation(image, 1.5)
&lt;/code&gt;&lt;/pre&gt;
&lt;img src="./image/kinmosa_contrast_nl_gamma.jpg" width="800px"&gt;
&lt;p&gt;以上に挙げた点がそれなりに解消していますね。ノイズは若干残ってしまっています。モスキートノイズに効果があるらしいAdaptive
Bilateral Filterを使ってみたいので元の論文（Zhang and Allebach,
2008）を読んで実装したい…。&lt;/p&gt;
&lt;p&gt;ガンマ変換はパラメータによってはノイズが出過ぎるので、やらなくてもいいかもしれません。&lt;/p&gt;
&lt;p&gt;他のキャプチャ画像でも色々線画を作ってみましたが、少なくとも私が試してみた範囲では、コード中のパラメータはそのままで問題なさそうです。&lt;/p&gt;
&lt;p&gt;&lt;a href="https://slowlooptv.com/" target="_blank" rel="noopener noreferrer"&gt;スローループ&lt;/a&gt;の海凪小春ちゃん&lt;/p&gt;
&lt;img src="./image/slowloop.jpg" width="800px"&gt;
&lt;img src="./image/slowloop_contrast_nl_gamma.jpg" width="800px"&gt;
&lt;p&gt;kawaii!コントラストがはっきりしている画像だと線画が綺麗に取り出せますね。&lt;/p&gt;
&lt;h2 id="線画から動画を作る"&gt;線画から動画を作る&lt;/h2&gt;
&lt;p&gt;このロジックとffmpegを用いて、アニメの動画を線画化することができます。&lt;/p&gt;
&lt;p&gt;流れは以下の通りです。1〜5をシェルスクリプトで書いて3のpythonコードをシェルスクリプト内で読み込むようにしました。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;（ffmpeg）元の動画から静止画を取り出す&lt;/li&gt;
&lt;li&gt;（ffmpeg）元の動画から音声を取り出す&lt;/li&gt;
&lt;li&gt;（上のpythonコード）1で取り出した静止画を線画に変換する&lt;/li&gt;
&lt;li&gt;（ffmpeg）3で作った線画を動画にする&lt;/li&gt;
&lt;li&gt;（ffmpeg）4で作った動画に2の音声を合わせて音声ありの動画にする&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;きんいろモザイク1期のOPの冒頭です。ただし5の音声は付けていません。&lt;/p&gt;
&lt;style&gt;video#player {width: 90%; max-width: 640px;}&lt;/style&gt;
&lt;div align="center"&gt;
&lt;video id="player" playsinline controls&gt;
&lt;source src="./kinmosa_op.mp4" type="video/mp4"&gt; &lt;/video&gt;
&lt;/div&gt;
&lt;h2 id="おわりに"&gt;おわりに&lt;/h2&gt;
&lt;p&gt;いい感じに線画が作れました。&lt;/p&gt;
&lt;p&gt;画像処理は素人なので多少勉強をしたのですが、こちらの本が参考になりました。画像処理のアルゴリズムを一通り解説しているものです。何も分からずにOpenCVの関数を使うのではなく、その背後にあるアルゴリズムを多少なりとも把握できるとより面白く感じられました。&lt;/p&gt;
&lt;p&gt;&lt;a href="https://www.amazon.co.jp/dp/490347464X" target="_blank" rel="noopener noreferrer"&gt;ディジタル画像処理 改訂第二版&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="参考"&gt;参考&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://qiita.com/khsk/items/6cf4bae0166e4b12b942" target="_blank" rel="noopener noreferrer"&gt;そこそこな線画を目指す OpenCV -
Qiita&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://testpy.hatenablog.com/entry/2018/03/18/042514" target="_blank" rel="noopener noreferrer"&gt;膨張差分法とキャニー法による線画の比較 -
test.py&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;B. Zhang and J. P. Allebach, “Adaptive Bilateral Filter for Sharpness
Enhancement and Noise Removal”, IEEE Transactions on Image Processing,
vol. 17, no. 5, pp. 664-678, 2008.&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>FastAPIアプリをGunicorn + Nginxで公開する</title><link>https://suzunano.net/posts/fastapi-nginx/</link><pubDate>Sun, 25 Dec 2022 00:00:00 +0900</pubDate><guid>https://suzunano.net/posts/fastapi-nginx/</guid><description>&lt;h2 id="はじめに"&gt;はじめに&lt;/h2&gt;
&lt;h3 id="概要"&gt;概要&lt;/h3&gt;
&lt;p&gt;PythonのAPIフレームワークであるFastAPIを用い、Ubuntu環境 (VPS) にFastAPI + Gunicorn + Nginxの構成でREST APIを作る際の設定方法です。特にNginxの設定がいつも分からなくてググっているのでメモしておきます。&lt;/p&gt;
&lt;p&gt;この記事の対象はNginxを全く触ったことないような方です。私のようなNginxが全く分からないPython使いがとりあえずFastAPIをNginxで公開できる所まで持っていこうという趣旨です。&lt;/p&gt;
&lt;h3 id="環境"&gt;環境&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Ubuntu 22.04.1 LTS（ConoHa VPS）&lt;/li&gt;
&lt;li&gt;nginx 1.18.0&lt;/li&gt;
&lt;li&gt;certbot 1.21.0&lt;/li&gt;
&lt;li&gt;Python: Miniconda環境
&lt;ul&gt;
&lt;li&gt;Miniconda 4.12.0&lt;/li&gt;
&lt;li&gt;Python 3.10.4&lt;/li&gt;
&lt;li&gt;FastAPI 0.79.0&lt;/li&gt;
&lt;li&gt;uvicorn 0.18.2&lt;/li&gt;
&lt;li&gt;gunicorn 20.1.0&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="fastapiアプリを公開するuvicorngunicorn"&gt;FastAPIアプリを公開する（Uvicorn/Gunicorn）&lt;/h2&gt;
&lt;h3 id="fastapiでapiエンドポイントの作成"&gt;FastAPIでAPIエンドポイントの作成&lt;/h3&gt;
&lt;p&gt;それでは今回デプロイするFastAPIアプリを作成します。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;$ pip install fastapi pydantic uvicorn[standard] gunicorn
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;以下のコードをVPS上の適当なディレクトリに&lt;code&gt;main.py&lt;/code&gt;というファイル名で保存します。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-Python"&gt;from fastapi import FastAPI
app = FastAPI(root_path=&amp;quot;/&amp;quot;)
@app.get(&amp;quot;/&amp;quot;)
def say_hello():
return {&amp;quot;message&amp;quot;: &amp;quot;Hello!&amp;quot;}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;ルートにGETすると&lt;code&gt;{&amp;quot;message&amp;quot;: &amp;quot;Hello!&amp;quot;}&lt;/code&gt;というJSONを返すAPIです。&lt;/p&gt;
&lt;h3 id="fastapiアプリの公開ローカル"&gt;FastAPIアプリの公開（ローカル）&lt;/h3&gt;
&lt;p&gt;まずはアプリケーションサーバにUvicornを用いて127.0.0.1:8000にこのAPIを立てます。ポート番号は好きな番号で構いませんが、とりあえず8000番ポートに立ててみます。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;$ cd [main.pyを保存したディレクトリのパス]
$ python -m uvicorn main:app --host 127.0.0.1 --port 8000
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;AnacondaやMiniconda環境の場合は、&lt;code&gt;conda activate [仮想環境名]&lt;/code&gt;してから上を実行するか、上の&lt;code&gt;python&lt;/code&gt;を、Anaconda/Miniconda環境で使用しているPythonのパスに置き換えます。このパスは、&lt;code&gt;conda activate [仮想環境名]; which python&lt;/code&gt;で知ることができます。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;別のターミナルを開き、VPS上で127.0.0.1:8000にcurlでGETして&lt;code&gt;{&amp;quot;message&amp;quot;: &amp;quot;Hello!&amp;quot;}&lt;/code&gt;というJSONが返ってくればAPIが立てられています。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;$ curl 127.0.0.1:8000
{&amp;quot;message&amp;quot;:&amp;quot;Hello!&amp;quot;}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;今はアプリケーションサーバにUvicornを用いましたが、Gunicornを用いてGunicornからUvicornを触ることができます。本番環境ではGunicornを用いる方がいいようなので、以下Gunicornを用いて説明します。&lt;/p&gt;
&lt;p&gt;Uvicornを直接用いる前述の場合は単一プロセスですが、Gunicornを用いるとUvicornを複数プロセス立ち上げることができ、またそのUvicornプロセスが落ちたとしても再度プロセスを自動で立ち上げてくれます。（詳細は公式ドキュメントを参照: &lt;a href="https://fastapi.tiangolo.com/deployment/server-workers/" target="_blank" rel="noopener noreferrer"&gt;Server Workers - Gunicorn with Uvicorn - FastAPI&lt;/a&gt;）&lt;/p&gt;
&lt;p&gt;以下のようにすることで、-wの引数で指定したプロセス数だけワーカーを持つようにGunicornが起動します。ワーカー数は適当に2にします。127.0.0.1:8000にcurlでGETすると同様に&lt;code&gt;{&amp;quot;message&amp;quot;: &amp;quot;Hello!&amp;quot;}&lt;/code&gt;が返ってきます。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;python -m gunicorn main:app --bind 127.0.0.1:8000 -w 2 -k uvicorn.workers.UvicornWorker
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id="fastapiアプリの公開グローバル"&gt;FastAPIアプリの公開（グローバル）&lt;/h3&gt;
&lt;p&gt;これまでは127.0.0.1にAPIを立てていました。以下のように0.0.0.0を指定することで、外部からアクセスできるようになります。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;# Uvicornの場合
$ python -m uvicorn main:app --host 0.0.0.0 --port 8000
# Gunicornの場合
$ python -m gunicorn main:app --bind 0.0.0.0:8000 -w 2 -k uvicorn.workers.UvicornWorker
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;指定したポート番号のポート（ここでは8000番）をファイアウォールで開けておいてください。&lt;/li&gt;
&lt;li&gt;特権ポートと呼ばれる1023番までのポート番号を指定する場合、sudo権限が必要です。先頭にsudoを付けてください。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;VPSではなく自分のローカルPCから&lt;code&gt;curl [VPSのIPアドレス]:8000&lt;/code&gt;を叩いてみて、同様に&lt;code&gt;{&amp;quot;message&amp;quot;:&amp;quot;Hello!&amp;quot;}&lt;/code&gt;が返ってくれば成功です。Webブラウザのアドレスバーに&lt;code&gt;[VPSのIPアドレス]:8000&lt;/code&gt;を入力して開いてみても構いません。&lt;/p&gt;
&lt;h2 id="nginxの概略"&gt;Nginxの概略&lt;/h2&gt;
&lt;p&gt;以上の内容でとりあえずFastAPIアプリを公開することができますが、以下ではAPIにアクセスしてくるユーザとUvicorn/Gunicornの間にWebサーバのNginxを入れようと思います。&lt;/p&gt;
&lt;p&gt;Nginxを入れない場合、複数のFastAPIアプリなどを公開しようとすると、アプリごとにポート番号を変える必要があります。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;http://x.x.x.x:8000&lt;/code&gt;でアプリ1にアクセスできる&lt;/li&gt;
&lt;li&gt;&lt;code&gt;http://x.x.x.x:8001&lt;/code&gt;でアプリ2にアクセスできる&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;一方、Nginxを入れてリバースプロキシすると、このようにサブディレクトリへのアクセスを振り分けることができます。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;http://127.0.0.1:8000&lt;/code&gt;でアプリ1を立ち上げる
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;http://hoge.example.com/app1/&lt;/code&gt;を&lt;code&gt;127.0.0.1:8000&lt;/code&gt;にリバースプロキシしてアプリ1にアクセスできる&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;http://127.0.0.1:8001&lt;/code&gt;でアプリ2を立ち上げる
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;http://hoge.example.com/app2/&lt;/code&gt;を&lt;code&gt;127.0.0.1:8001&lt;/code&gt;にリバースプロキシしてアプリ2にアクセスできる&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;この次の章では、先程作成したFastAPIアプリを&lt;code&gt;127.0.0.1:8000&lt;/code&gt;で立ち上げておき、&lt;code&gt;http://（VPSのIPアドレス）/app&lt;/code&gt;にGETするとNginxのリバースプロキシで&lt;code&gt;127.0.0.1:8000&lt;/code&gt;に転送され、先程のアプリがレスポンスを返すようにします。この章では、その前にNginxの設定ファイルについて簡単に説明します。&lt;/p&gt;
&lt;h3 id="nginxのインストール"&gt;Nginxのインストール&lt;/h3&gt;
&lt;p&gt;まずNginxをインストールします。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;$ sudo apt install nginx
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;1行目でNginxを起動し、2行目でUbuntuの起動時にNginxが自動で起動するようにします。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;$ sudo systemctl start nginx
$ sudo systemctl enable nginx
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;起動できているか、また自動起動が有効になっているかを確認します。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;$ sudo systemctl status nginx
nginx.service - A high performance web server and a reverse proxy server
Loaded: loaded (/lib/systemd/system/nginx.service; enabled; vendor preset: enabled)
Active: active (running) since Sun 2022-12-25 20:43:29 JST; 25s ago
（以下略）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Active: active (running)&lt;/code&gt;とあるのが今起動できていること、&lt;code&gt;/lib/systemd/system/nginx.service; enabled&lt;/code&gt;とあるのが自動起動されていることを示します。&lt;/p&gt;
&lt;p&gt;なお、起動しているNginxを停止したい場合は&lt;code&gt;sudo systemctl stop nginx&lt;/code&gt;、自動起動を無効にしたい場合は&lt;code&gt;sudo systemctl disable nginx&lt;/code&gt;です。&lt;/p&gt;
&lt;h3 id="nginxの設定ファイル"&gt;Nginxの設定ファイル&lt;/h3&gt;
&lt;p&gt;バーチャルホストを作るときに使うNginxの設定ファイルの構成は以下のようになっています。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;/etc/nginx/nginx.conf&lt;/li&gt;
&lt;li&gt;/etc/nginx/conf.d/*.conf&lt;/li&gt;
&lt;li&gt;/etc/nginx/sites-enabled/*&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;バーチャルホストを追加する際は、&lt;code&gt;/etc/nginx/nginx.conf&lt;/code&gt;は編集せず、&lt;code&gt;/etc/nginx/conf.d/*.conf&lt;/code&gt;か&lt;code&gt;/etc/nginx/sites-enabled/*&lt;/code&gt;に追加します。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;/etc/nginx/nginx.conf&lt;/code&gt;には以下のように記載されていることから、&lt;code&gt;/etc/nginx/nginx.conf&lt;/code&gt;がロードされるときに、その中で&lt;code&gt;/etc/nginx/conf.d/&lt;/code&gt;直下にある拡張子confのファイルと&lt;code&gt;/etc/nginx/sites-enabled/&lt;/code&gt;直下にあるファイルが読み込まれることが分かります。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-text"&gt;http {
（略）
##
# Virtual Host Configs
##
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;/etc/nginx/sites-enabled/*&lt;/code&gt;に追加する際は、実際には&lt;code&gt;/etc/nginx/sites-available/&lt;/code&gt;直下に設定を作成し、それを&lt;code&gt;/etc/nginx/sites-enabled/&lt;/code&gt;にシンボリックリンクを張るようにするのが一般的です。シンボリックリンクを外せばNginxの設定から除外されるのがメリットです。&lt;/p&gt;
&lt;p&gt;それでは&lt;code&gt;/etc/nginx/conf.d/*.conf&lt;/code&gt;と&lt;code&gt;etc/nginx/sites-available/*&lt;/code&gt;のどちらに設定を記載すればよいかですが、多くのバーチャルホストを使う場合、あるいはバーチャルホストをデプロイしたりしなかったりと切り替えたい場合は後者、そうではない場合は前者、のような考え方が一つの決め方になります。本記事では、&lt;code&gt;/etc/nginx/sites-available/&lt;/code&gt;以下に設定を作ることにします。&lt;/p&gt;
&lt;h2 id="fastapiアプリを公開するgunicorn--nginx"&gt;FastAPIアプリを公開する（Gunicorn + Nginx）&lt;/h2&gt;
&lt;p&gt;Nginxを用いたリバースプロキシでのAPIの公開について説明します。&lt;/p&gt;
&lt;p&gt;まず、&lt;code&gt;/etc/nginx/sites-available/&lt;/code&gt;直下に設定ファイルを作成します。この記事では&lt;code&gt;/etc/nginx/sites-available/fastapi&lt;/code&gt;というファイルを作ることにします。デフォルトのファイルとして&lt;code&gt;/etc/nginx/sites-available/default&lt;/code&gt;が用意されていますので、これをひな形としてコピーしてから編集することにします。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;$ sudo cp /etc/nginx/sites-available/default /etc/nginx/sites-available/fastapi
$ sudo ln -s /etc/nginx/sites-available/fastapi /etc/nginx/sites-enabled/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;2行目のシンボリックリンクを貼る作業は、シンボリックリンクを外さない限りは&lt;code&gt;/etc/nginx/sites-available/&lt;/code&gt;に新しいファイルを作成したら最初の1回だけ行っておけば大丈夫です。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;sudo nano /etc/nginx/sites-available/fastapi&lt;/code&gt;でこのファイルを編集します。以下を貼り付けて上書き保存します。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-text"&gt;server {
listen 80;
location /app/ {
proxy_pass http://127.0.0.1:8000/;
}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;VPSのIPアドレスを&lt;code&gt;x.x.x.x&lt;/code&gt;とします。80番ポートをlistenするよ、&lt;code&gt;http://x.x.x.x:80/app/&lt;/code&gt;に来たアクセスは&lt;code&gt;127.0.0.1:8000&lt;/code&gt;に転送するよということですね。&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.cosnomi.com/posts/674/" target="_blank" rel="noopener noreferrer"&gt;なお、proxy_passのtrailing slash（末尾のスラッシュ）は付けるようにしましょう。&lt;/a&gt;付けないと正しくアクセスできません。地味にハマりポイントです。&lt;/p&gt;
&lt;p&gt;保存したら、&lt;code&gt;sudo nginx -t&lt;/code&gt;を実行してNginxの設定ファイルに構文エラーがないかどうかを確かめておきます。&lt;/p&gt;
&lt;p&gt;エラーが表示されなければ構文に誤りはありませんので、Nginxを再起動することで今作成した設定ファイルを反映させます。設定ファイルを更新したら必ずNginxを再起動してください。再起動するまでは反映されません。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;$ sudo systemctl stop nginx
$ sudo systemctl start nginx
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;次に、先程作成したFastAPIアプリのroot_pathを&lt;code&gt;/app/&lt;/code&gt;に変更します。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-Python"&gt;from fastapi import FastAPI
app = FastAPI(root_path=&amp;quot;/app/&amp;quot;)
@app.get(&amp;quot;/&amp;quot;)
def say_hello():
return {&amp;quot;message&amp;quot;: &amp;quot;Hello!&amp;quot;}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;このFastAPIのエンドポイントをGunicornで公開します。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;$ python -m gunicorn main:app --bind 127.0.0.1:8000 -w 2 -k uvicorn.workers.UvicornWorker
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;ローカルのPCから&lt;code&gt;http://x.x.x.x/app/&lt;/code&gt;をブラウザで開くかcurlを叩いて&lt;code&gt;{&amp;quot;message&amp;quot;:&amp;quot;Hello!&amp;quot;}&lt;/code&gt;が返ってくれば成功です。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;http://x.x.x.x/&lt;/code&gt;で公開したい場合は、FastAPIアプリのroot_pathを&lt;code&gt;/app/&lt;/code&gt;ではなく&lt;code&gt;/&lt;/code&gt;にし、&lt;code&gt;/etc/nginx/sites-available/fastapi&lt;/code&gt;のlocationも&lt;code&gt;/app/&lt;/code&gt;ではなく&lt;code&gt;/&lt;/code&gt;にします。&lt;/p&gt;
&lt;h2 id="nginxのより進んだ設定"&gt;Nginxのより進んだ設定&lt;/h2&gt;
&lt;p&gt;ここまででFastAPIアプリをGunicorn + Nginxで公開することができました。&lt;/p&gt;
&lt;p&gt;FastAPIアプリを本番公開する際には、Nginxの設定において追加でいくつか行った方がいいことがあります。以下順に説明していきます。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;SSL化
&lt;ol&gt;
&lt;li&gt;Let&amp;rsquo;s Encript + certbotでSSL証明書の導入と自動更新&lt;/li&gt;
&lt;li&gt;SSL対応 + HTTPに来たアクセスのリダイレクト&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;IPアドレス直打ちでのアクセスを拒否&lt;/li&gt;
&lt;li&gt;Nginxのバージョンを非表示&lt;/li&gt;
&lt;li&gt;IPv6対応&lt;/li&gt;
&lt;li&gt;アクセスログにリバースプロキシを考慮したIPアドレスを残す&lt;/li&gt;
&lt;li&gt;アクセスログにPOSTボディを出す&lt;/li&gt;
&lt;li&gt;アクセスログを別ファイル化&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="1-ssl化"&gt;1. SSL化&lt;/h3&gt;
&lt;h4 id="ssl証明書の導入自動更新の設定"&gt;SSL証明書の導入・自動更新の設定&lt;/h4&gt;
&lt;p&gt;この節は独自ドメインのSSL証明書を取る時の最初の1回だけ行います。&lt;/p&gt;
&lt;p&gt;独自ドメインを取得していることを前提にします。以下、&lt;code&gt;hoge.example.com&lt;/code&gt;という独自ドメインを使いたいとします。まず、独自ドメインを取得したドメイン会社のDNSレコード設定ページから、独自ドメインとVPSサーバのIPアドレスを紐づけてください。&lt;/p&gt;
&lt;p&gt;sshの証明書はLet&amp;rsquo;s Encriptで取ることにします。証明書は90日おきに更新する必要がありますが、certbotを入れておくと自動で更新してくれます。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;$ sudo apt install -y certbot python3-certbot-nginx
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;certbotの設定をします。以下を実行すると、恐らくメールアドレスを入力するようにメッセージが出ると思いますので、その通り入力してください。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;$ sudo certbot --nginx -d hoge.example.com
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;次に、証明書の90日おきの自動更新が機能しているかどうか確かめます。以下を実行してエラーが出なければOKです。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;$ sudo certbot renew --dry-run
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id="nginxの設定ファイルの対応"&gt;Nginxの設定ファイルの対応&lt;/h4&gt;
&lt;p&gt;ここまでで独自ドメインのSSL化ができました。次にNginxの設定を行います。&lt;/p&gt;
&lt;p&gt;独自ドメインを使いたいアプリの設定が記述されているファイル（&lt;code&gt;/etc/nginx/sites-available/fastapi&lt;/code&gt;など）に以下を貼り付けて上書き保存します。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-text"&gt;server {
server_name hoge.example.com;
listen 443 ssl default_server;
ssl_certificate /etc/letsencrypt/live/hoge.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/hoge.example.com/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
（略）
}
server {
if ($host = hoge.example.com) {
return 301 https://$host$request_uri;
}
listen 80;
server_name hoge.example.com;
return 404;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;1個目のserverディレクティブでは、80番ポートだけでなく443番ポートもlistenするように設定します。また、SSLの証明書のパスなどを設定しています。&lt;/li&gt;
&lt;li&gt;2個目のserverディレクティブでは、80番ポートへのアクセスを443番ポートにリダイレクトしています。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;なお、default_serverの挙動ですが、デフォルトでは複数の設定ファイルを読み込んでいるときはファイル名の順番に読み込み、一番最初に読み込まれた設定ファイルに記載されているserver_nameをdefault_serverとします。&lt;/p&gt;
&lt;p&gt;上で記載したように同一のserverディレクティブ内にserver_nameを指定して&lt;code&gt;listen [port] default_server&lt;/code&gt;と記載すると、このserverディレクティブ内のserver_nameをdefault_serverとします。&lt;/p&gt;
&lt;h3 id="2-ip直打ち拒否"&gt;2. IP直打ち拒否&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;x.x.x.x&lt;/code&gt;宛のアクセスを拒否します。ユーザがWebサーバにIPアドレス直打ちでアクセスしてくることは普通考えにくいためです。&lt;/p&gt;
&lt;p&gt;設定を反映させたい設定ファイルに以下を記述します。性質上、全てのバーチャルホストで同じ設定をしたいケースが多いと思いますので、個別の設定ファイルである&lt;code&gt;/etc/nginx/sites-available/fastapi&lt;/code&gt;ではなく、&lt;code&gt;/etc/nginx/nginx.conf&lt;/code&gt;か&lt;code&gt;/etc/nginx/conf.d/default.conf&lt;/code&gt;に以下の通り記述するのでも構いません。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-text"&gt;server {
server_name _;
listen 80 default_server;
listen 443 ssl default_server;
return 444;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Nginxは、Hostヘッダがどのサーバ名ともマッチしないとき、あるいはリクエストにHostヘッダが含まれていないときはデフォルトサーバに振り分けます。これによって、他の設定ファイルのserverディレクティブ内に記載されているドメイン以外のアクセスは444エラーを返します。&lt;/p&gt;
&lt;p&gt;server_nameのアンダーバーは「全てのサーバ」を示します。他の設定ファイルで定義されている&lt;code&gt;hoge.example.com&lt;/code&gt;などのドメインに該当しなかった全てのアクセスをこのserverディレクティブでキャッチするということですね。&lt;/p&gt;
&lt;h3 id="3-nginxのバージョンを非表示"&gt;3. Nginxのバージョンを非表示&lt;/h3&gt;
&lt;p&gt;Nginxはデフォルトでは使っているバージョンを表示します。特定のバージョンに脆弱性があり、自分が使っているバージョンが脆弱性のあるバージョンの場合、侵入者に脆弱性を知らせてしまっていますから、バージョンを非表示にするのが望ましいです。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;/etc/nginx/nginx.conf&lt;/code&gt;内に以下の1行を記載すればOKです。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-text"&gt;http {
（略）
server_tokens off;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;あるいは、&lt;code&gt;/etc/nginx/sites-available/fastapi&lt;/code&gt;などの個別の設定ファイル内に以下のように記載しても構いません。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-text"&gt;server {
（略）
server_tokens off;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id="4-ipv6対応"&gt;4. IPv6対応&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;/etc/nginx/sites-available/fastapi&lt;/code&gt;に以下を記載します。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-text"&gt;server {
（略）
# ここから下2行はIPv4対応
listen 80 default_server;
listen 443 ssl default_server;
# ここから下2行はIPv6対応
listen [::]:80 default_server;
listen [::]:443 ssl default_server;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id="5-ログにリバースプロキシを考慮した接続元のipアドレスを残す"&gt;5. ログにリバースプロキシを考慮した接続元のIPアドレスを残す&lt;/h3&gt;
&lt;p&gt;リバースプロキシしているので、何も設定しないとログファイルに残るIPアドレスなどは自分のIPアドレスになってしまいます。外部からアクセスしてきたIPアドレスなどをそのまま残すには、&lt;code&gt;/etc/nginx/sites-available/fastapi&lt;/code&gt;のlocationディレクティブの中に、&lt;code&gt;proxy_set_header&lt;/code&gt;で始まる5行を書きます。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-text"&gt;server {
（略）
location /app/ {
proxy_pass http://127.0.0.1:8000/;
# ここから
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Server $host;
proxy_set_header X-Real-IP $remote_addr;
# ここまで
}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id="6-ログにpostボディを出す"&gt;6. ログにPOSTボディを出す&lt;/h3&gt;
&lt;p&gt;Nginxのデフォルトの設定では、ログにPOSTボディの中身は表示されません。以下のようにすると表示できます。&lt;/p&gt;
&lt;p&gt;ただし、POSTボディが長い文字列になる場合、ログファイルが圧迫されてしまうことに注意してください。&lt;/p&gt;
&lt;p&gt;まず、&lt;code&gt;/etc/nginx/nginx.conf&lt;/code&gt;にコメントを付した3行を記載します。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-text"&gt;http {
##
# Logging Settings
##
log_format '$remote_addr - $remote_user [$time_local] &amp;quot;$request&amp;quot; '
'$status $body_bytes_sent &amp;quot;$http_referer&amp;quot; '
'&amp;quot;$http_user_agent&amp;quot; &amp;quot;$http_x_forwarded_for&amp;quot;';
# 以下の3行を記載する
log_format format1 '$remote_addr - $remote_user [$time_local] &amp;quot;$request&amp;quot; '
'$status $body_bytes_sent &amp;quot;$http_referer&amp;quot; '
'&amp;quot;$http_user_agent&amp;quot; &amp;quot;$http_x_forwarded_for&amp;quot; &amp;quot;$request_body&amp;quot;';
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;次に、&lt;code&gt;/etc/nginx/sites-available/fastapi&lt;/code&gt;内に次の1行を追加します。これにより、&lt;code&gt;/etc/nginx/nginx.conf&lt;/code&gt;で定義したformat1の形式でログが記載されます。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-text"&gt;server {
location /app/ {
（略）
# 追加する
access_log /var/log/nginx/access.log format1;
}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id="7-ログを別ファイル化"&gt;7. ログを別ファイル化&lt;/h3&gt;
&lt;p&gt;デフォルトでは&lt;code&gt;/etc/nginx/nginx.conf&lt;/code&gt;の&lt;code&gt;access_log&lt;/code&gt;に記載の&lt;code&gt;/var/log/nginx/access.log&lt;/code&gt;にログが作られますが、変更することもできます。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;/etc/nginx/sites-available/fastapi&lt;/code&gt;に以下を追記します。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-text"&gt;server {
location /app/ {
（略）
# 追加する
access_log /var/log/nginx/access_fastapi.log;
}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;もちろん、上で説明したようにログのフォーマットを変えることもできます。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-text"&gt;server {
location /app/ {
（略）
# 追加する
access_log /var/log/nginx/access_fastapi.log format1;
}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id="参考"&gt;参考&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;公式ドキュメント
&lt;ul&gt;
&lt;li&gt;FastAPI + Uvicorn / Gunicorn
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://fastapi.tiangolo.com/deployment/server-workers/" target="_blank" rel="noopener noreferrer"&gt;Server Workers - Gunicorn with Uvicorn - FastAPI&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Nginx
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.nginx.com/resources/wiki/start/" target="_blank" rel="noopener noreferrer"&gt;Getting Started | NGINX&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;その他参考にさせていただいた記事（Nginx）
&lt;ul&gt;
&lt;li&gt;Nginxを導入したFastAPIの公開方法
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.vultr.com/docs/how-to-deploy-fastapi-applications-with-gunicorn-and-nginx-on-ubuntu-20-04/" target="_blank" rel="noopener noreferrer"&gt;How to Deploy FastAPI Applications with Gunicorn and Nginx on Ubuntu 20.04 - Vultr.com&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://senablog.com/python-fastapi-nginx/" target="_blank" rel="noopener noreferrer"&gt;【Python3】FastAPI+Uvicorn+Nginxで環境構築を行う方法 | せなブログ&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Nginxの設定ファイルについて
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://qiita.com/tomokon/items/e782636c1e5ec6b5dfdc" target="_blank" rel="noopener noreferrer"&gt;Nginxのconf.dとsites-availableとsites-enabledの違い - Qiita&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://office54.net/iot/linux/nginx-setting-file" target="_blank" rel="noopener noreferrer"&gt;【Nginx】設定ファイルとは（nginx.conf、conf.d、sites-available、uwsgi_paramsなど） | OFFICE54&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://qiita.com/tajihiro/items/dcc3cef60812852ca54c" target="_blank" rel="noopener noreferrer"&gt;Nginxのバーチャルホスト設定 - Qiita&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;NginxのTips
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://mahori.jp/nginx-default-server/" target="_blank" rel="noopener noreferrer"&gt;nginxでデフォルトサーバを指定する – mahori blog&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.cosnomi.com/posts/674/" target="_blank" rel="noopener noreferrer"&gt;nginxでproxy_passのtrailing slashには特に気をつけるべきという話 | Cosnomi Blog&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://qiita.com/ngron/items/55a84ef6abce903c4424" target="_blank" rel="noopener noreferrer"&gt;【Nginx】portが占領されてサーバーが起動できないときの対処法 - Qiita&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>GPT-2で作ったConoHa上のこのはちゃんbotとSlackで会話する</title><link>https://suzunano.net/posts/conoha-chatbot/</link><pubDate>Sun, 11 Dec 2022 00:00:00 +0900</pubDate><guid>https://suzunano.net/posts/conoha-chatbot/</guid><description>&lt;h2 id="はじめに"&gt;はじめに&lt;/h2&gt;
&lt;p&gt;この記事は&lt;a href="https://qiita.com/advent-calendar/2022/conoha" target="_blank" rel="noopener noreferrer"&gt;ConoHa Advent Calendar 2022&lt;/a&gt;の11日目の記事です。&lt;/p&gt;
&lt;p&gt;ConoHa Advent Calendarは初めての投稿です。どうぞよろしくお願いいたします。&lt;/p&gt;
&lt;p&gt;ConoHa、いいですよね。課金が時間単位、転送量課金がない、スケールアップ・スケールダウンが可能、と使い勝手がいいですが、何より&lt;a href="https://conoha.mikumo.com/" target="_blank" rel="noopener noreferrer"&gt;美雲このはちゃん&lt;/a&gt;が清楚かわいいのでモチベーションが上がります。&lt;/p&gt;
&lt;p&gt;Advent Calendarの記事のテーマを考えながらConoHa上で作業をしていたときにふと思いました。ConoHaでの作業の合間にこのはちゃんとおしゃべりできたら楽しそうだなと。個人的にちょうどGPT-2にも興味を持っていたのです。技術の力で何とかなるかもしれませんね？&lt;/p&gt;
&lt;p&gt;というわけで、自然言語処理における深層学習モデルの一種であるGPT-2を利用して、文章を入力すると「このはちゃんっぽい」返事を出力するモデル（このはちゃんモデル）を作成しました。このモデルを組み込んだSlackのチャットボットのAPIをConoHa VPS上に立て、Slackでこのはちゃんbotとおしゃべりしてみました。なお、このはちゃんモデルを作成する際には、Twitterのこのはちゃん（&lt;a href="https://twitter.com/MikumoConoHa" target="_blank" rel="noopener noreferrer"&gt;@MikumoConoHa&lt;/a&gt;）へのメンションのツイートとそれに対するこのはちゃんのリプライのテキストデータを用いています。&lt;/p&gt;
&lt;p&gt;技術的には、GPT-2の推論モデルを組み込んだSlack botのAPIをConoHa上にFastAPI + Boltで立てました。FastAPIはPythonのAPIフレームワーク、BoltはSlack botを作れるSlack公式のライブラリです。モデルはrinna社の日本語の事前学習済みGPT-2モデルであるjapanese-gpt2-smallをツイートデータでファインチューニングすることで作成しています。&lt;/p&gt;
&lt;p&gt;&lt;img src="./images/architecture.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;記事の流れは以下の通りです。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;学習データの入手（ローカルPC）
&lt;ol&gt;
&lt;li&gt;ツイートを収集する&lt;/li&gt;
&lt;li&gt;1のテキストを前処理する&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;このはちゃんモデルの作成（ローカルPC）
&lt;ol&gt;
&lt;li&gt;ローカルPCに環境を構築する&lt;/li&gt;
&lt;li&gt;ファインチューニングする&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;このはちゃんモデルを組み込んだSlack botのAPIをデプロイ（ConoHa VPS）
&lt;ol&gt;
&lt;li&gt;Slack APIのWebサイトよりEvent Subscription型のSlackアプリを作成する&lt;/li&gt;
&lt;li&gt;2-2で作成したこのはちゃんモデルを組み込んだSlack botのAPIをVPSにデプロイする&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="環境"&gt;環境&lt;/h2&gt;
&lt;h3 id="ローカルpc"&gt;ローカルPC&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;OS、ハード
&lt;ul&gt;
&lt;li&gt;Windows 10&lt;/li&gt;
&lt;li&gt;NVIDIA GeForce RTX 2060 Super&lt;/li&gt;
&lt;li&gt;CUDA 11.6&lt;/li&gt;
&lt;li&gt;CuDNN 8.5.0&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Python
&lt;ul&gt;
&lt;li&gt;python 3.10.4 (miniconda 4.10.3)&lt;/li&gt;
&lt;li&gt;torch 1.12.1+cu116&lt;/li&gt;
&lt;li&gt;transformers 4.22.0.dev0&lt;/li&gt;
&lt;li&gt;sentencepiece 0.1.97&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;R
&lt;ul&gt;
&lt;li&gt;R 4.2.1 (RStudio 2022.07.1+554 Spotted Wakerobin (desktop))&lt;/li&gt;
&lt;li&gt;rtweet 1.0.2&lt;/li&gt;
&lt;li&gt;rvest 1.0.3&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="conoha-vpsメモリ2gb"&gt;ConoHa VPS（メモリ2GB）&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;OS
&lt;ul&gt;
&lt;li&gt;Ubuntu 22.04.1 LTS&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Python
&lt;ul&gt;
&lt;li&gt;python 3.10.4 (miniconda 4.12.0)&lt;/li&gt;
&lt;li&gt;fastapi 0.79.0&lt;/li&gt;
&lt;li&gt;slack-bolt 1.14.3&lt;/li&gt;
&lt;li&gt;gunicorn 20.1.0&lt;/li&gt;
&lt;li&gt;torch, transformers, sentencepieceはローカルPCと同じ&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="学習データの入手"&gt;学習データの入手&lt;/h2&gt;
&lt;p&gt;（この章はConoHa VPSを使っていないので読み飛ばしていただいても構いません）&lt;/p&gt;
&lt;h3 id="ツイートの収集"&gt;ツイートの収集&lt;/h3&gt;
&lt;p&gt;まずは後のファインチューニングの学習データとして使用するツイートを集めます。&lt;/p&gt;
&lt;p&gt;いま作りたいチャットボットは、何かしらの問いかけをするとそれに対してこのはちゃんbotが返事をしてくれるというものです。ですから、学習データとして、@MikumoConoHaに対するリプライツイートと、それに対する@MikumoConoHaによるリプライのペアを集めればよいことになります。このようなツイートのペアを取得するには、まず@MikumoConoHaのツイートを取得し、次にツイートごとにツイートがリプライの場合はリプライ元のツイートを取得することになります。&lt;/p&gt;
&lt;p&gt;ロジックは以下の通りです。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;@MikumoConoHaのツイートの取得
&lt;ul&gt;
&lt;li&gt;過去のツイートを保存している&lt;a href="https://twilog.org/" target="_blank" rel="noopener noreferrer"&gt;twilog&lt;/a&gt;というWebサイトの&lt;a href="https://twilog.org/MikumoConoHa" target="_blank" rel="noopener noreferrer"&gt;@MikumoConoHaのページ&lt;/a&gt;より@MikumoConoHaのツイートをスクレイピングします。
&lt;ul&gt;
&lt;li&gt;Twitter APIを用いれば指定したユーザのツイートを取得することができます。
&lt;ul&gt;
&lt;li&gt;rtweet（RのTwitter APIクライアント）では&lt;code&gt;rtweet::get_timeline&lt;/code&gt;、tweepy（PythonのTwitter APIクライアント）では&lt;code&gt;tweepy.API.user_timeline&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;しかし、無料版では最新3200件しか取得できません。&lt;/li&gt;
&lt;li&gt;twilogには3200件の制約なく過去のツイートが掲載されているため、この方法をとりました。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;@MikumoConoHaのツイートが他のツイートへのリプライである場合、そのリプライ元のツイートのIDの取得
&lt;ul&gt;
&lt;li&gt;上のスクレイピングで取得した@MikumoConoHaの各ツイートのID（&lt;code&gt;https://twitter.com/&amp;lt;user_name&amp;gt;/status/[0-9]+&lt;/code&gt;の&lt;code&gt;[0-9]+&lt;/code&gt;）を用いてTwitter APIを叩くことで、各ツイートのテキストやメタ情報を取得します。メタ情報の中にはリプライ元のツイートのIDが&lt;code&gt;in_reply_to_status_id&lt;/code&gt;として含まれていますので、これを取り出します（ツイートが他のツイートに対するリプライでない場合はNULL）。
&lt;ul&gt;
&lt;li&gt;rtweetでは&lt;code&gt;rtweet::lookup_tweet&lt;/code&gt;、tweepyでは&lt;code&gt;tweepy.API.get_status&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;細かい話ですが、非公式RTなどでは&lt;code&gt;in_reply_to_status_id&lt;/code&gt;がNULLになることがあるようです。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;リプライ元のツイートのテキストの取得
&lt;ul&gt;
&lt;li&gt;上で入手したリプライ元のツイートのIDを用いて同じAPIをもう一度叩くことでリプライ元のツイートのテキストを得ます。
&lt;ul&gt;
&lt;li&gt;なお、非公開アカウントからのツイートである場合は得られません。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;ローカルPC上で、R（rtweet + rvest）で取得しました。記事の他の部分はPythonを用いているのでこの節もPython（tweepy + requests + beautifulsoup）で書いて言語を統一してもいいのですが、以前Rで似たようなコードを書いていたのでそれを流用しています。スクレイピングやクローリングの定期実行はVPSの得意とする所ですが、今回は数時間、1回のクローリングでデータが得られるためローカルPC上で実行しています。&lt;/p&gt;
&lt;h3 id="テキストの前処理"&gt;テキストの前処理&lt;/h3&gt;
&lt;p&gt;ここまでで入手したツイートのペアのテキストを前処理します。前処理あるあるだと思いますが、今回の記事で一番大変な工程でした。&lt;/p&gt;
&lt;p&gt;まずはツイートからメンション記号（@）やリツイート記号（RT）などを取り除き、純粋なテキスト部分を取り出します。リツイートは複数連鎖していたり、メンション記号が複数付いていたりするので、正規表現で頑張って取り除きます。&lt;/p&gt;
&lt;p&gt;そのうえで、通常のテキストの前処理を行います。全角チルダを波ダッシュに置換（いわゆる全角チルダ・波ダッシュ問題）、絵文字や顔文字、ハッシュタグの削除、NFKC正規化、記号の表記ゆれの統一（「、、」を「…」に置換するなど）を行っています。&lt;/p&gt;
&lt;p&gt;ここまでできたら、後述のモデルに投入するために、リプライ元とリプライのツイートの各ペアを&lt;/p&gt;
&lt;p&gt;&lt;code&gt;&amp;lt;s&amp;gt;（リプライ元のツイートのテキスト）[SEP]（それに対する@MikumoConoHaのリプライのテキスト）&amp;lt;/s&amp;gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;という形式で1行ずつ書き出したUTF-8のテキストファイルで出力します。ちなみに、リプライが複数往復している場合は複数行に切り分けられます。&lt;/p&gt;
&lt;p&gt;例えば、こちらのこのはちゃんとあんずちゃんの微笑ましい（？）やりとりから、&lt;/p&gt;
&lt;blockquote class="twitter-tweet"&gt;&lt;p lang="ja" dir="ltr"&gt;やだねっ！&lt;/p&gt;&amp;mdash; 美雲このは☁️💙 (@MikumoConoHa) &lt;a href="https://x.com/MikumoConoHa/status/1397713242168827907?ref_src=twsrc%5Etfw"&gt;May 27, 2021&lt;/a&gt;&lt;/blockquote&gt;
&lt;script async src="https://platform.x.com/widgets.js" charset="utf-8"&gt;&lt;/script&gt;
&lt;p&gt;以下の学習データが作成されます。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-text"&gt;&amp;lt;s&amp;gt;疲れちゃったこのはちゃんも手伝って〜![SEP]やだねっ!&amp;lt;/s&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;（絵文字を単純に削除したせいで「疲れちゃった」と「このはちゃん」がくっついてしまい、「このはちゃんが疲れた」ようにも読めますね。前処理の難しい所です。）&lt;/p&gt;
&lt;p&gt;このテキストがペアの数だけ行として存在します。以上により、リプライ元のツイートと@MikumoConoHaのリプライのペアを約23000件（約2.5MB）集めることができました。&lt;/p&gt;
&lt;h2 id="このはちゃんモデルの作成"&gt;このはちゃんモデルの作成&lt;/h2&gt;
&lt;p&gt;（この章もConoHa VPSを使っていないので読み飛ばしていただいても構いません）&lt;/p&gt;
&lt;p&gt;今回用いた手法であるGPT-2では、巨大な言語コーパスを学習データとした汎用的なモデル（事前学習モデル）をそのまま解きたいタスクに適用することもできますし、解きたいタスクのドメインに関する比較的少量のテキストを用いて事前学習モデルをファインチューニングすることでタスクに特化したモデルを作成することもできます。&lt;/p&gt;
&lt;p&gt;一般に、「汎用的なモデル」を一から作るには膨大な計算資源が必要ですので、既に公開されているモデルを利用するのが定番です。事前学習モデルは&lt;a href="https://www.rinna.jp/profile" target="_blank" rel="noopener noreferrer"&gt;りんなちゃん&lt;/a&gt;のrinna社が公開している日本語のGPT-2モデルである&lt;a href="https://github.com/rinnakk/japanese-pretrained-models" target="_blank" rel="noopener noreferrer"&gt;rinnakk/japanese-pretrained-models&lt;/a&gt;のjapanese-gpt2-smallというモデルを用いました。よりサイズが大きいモデルも公開されていますが、私のローカルPCのGPUではメモリに載らなかったため、japanese-gpt2-smallを用いました。&lt;/p&gt;
&lt;p&gt;このjapanese-gpt2-smallを先程作成したツイートデータでファインチューニングすることで、文章を入力するとそれに対するこのはちゃんっぽい文章を出力する（これが入力した文章に対する返信ということです）という今回解きたいタスクに特化したモデルを作るという流れです。&lt;/p&gt;
&lt;p&gt;ConoHa VPSにはGPUインスタンスがないため、ファインチューニングはローカルPCで行い、できたモデルをConoHaに持っていくことにします。深層学習は素人なため、誤りがあったらすみません。&lt;/p&gt;
&lt;h3 id="環境構築"&gt;環境構築&lt;/h3&gt;
&lt;p&gt;まずローカルPCにPyTorchとCUDA, CuDNNの環境を作ります。CUDAとCuDNNはtorchでGPUを使うのに必要なものです。&lt;/p&gt;
&lt;p&gt;環境構築はこちらの記事を参考にさせていただきました（ただし、この参考記事と違いPyTorch1.12 + CUDA 11.6 + CuDNN 8.5を入れました）。&lt;/p&gt;
&lt;p&gt;&lt;a href="https://zenn.dev/opamp/articles/c5e200c6b75912" target="_blank" rel="noopener noreferrer"&gt;Windows10にPyTorch1.10とCUDA11.3の環境を作る&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;CUDAとCuDNNを入れたら、以下を実行します。私はMinicondaの仮想環境の中でpipを用いています。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;$ conda create -n conoha-chatbot python=3.10
$ conda activate conoha-chatbot
$ conda install pip
# 参照: [Start Locally | PyTorch](https://pytorch.org/get-started/locally/)
$ pip install torch torchvision torchaudio --extra-index-url https://download.pytorch.org/whl/cu116
# protobufのバージョンを3.20以下にするようにエラーが出るのでprotobufは3.20のバージョンを指定
$ pip install pip install sentencepiece datasets evaluate protobuf==3.20
$ pip install git+https://github.com/huggingface/transformers
$ cd &amp;lt;適当な作業ディレクトリ&amp;gt;
# あとでファインチューニングでスクリプトを使うため
$ git clone https://github.com/huggingface/transformers
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id="ファインチューニング"&gt;ファインチューニング&lt;/h3&gt;
&lt;p&gt;こちらの記事を参考にさせていただきました。&lt;/p&gt;
&lt;p&gt;&lt;a href="https://qiita.com/m__k/items/36875fedf8ad1842b729#%E3%83%95%E3%82%A1%E3%82%A4%E3%83%B3%E3%83%81%E3%83%A5%E3%83%BC%E3%83%8B%E3%83%B3%E3%82%B0" target="_blank" rel="noopener noreferrer"&gt;GPT-2をファインチューニングしてニュース記事のタイトルを条件付きで生成してみた。 - Qiita&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;ファインチューニング用のファイルであるtransformersのrun_clm.pyの引数に先程作成した学習データとパラメータを渡せばOKです。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;$ cd &amp;lt;作業ディレクトリ&amp;gt;
$ python ./transformers/examples/pytorch/language-modeling/run_clm.py \
--model_name_or_path=rinna/japanese-gpt2-small \
# 先程出力したデータのテキストファイルのファイル名
--train_file=conoha_training_data.txt \
--validation_file=conoha_training_data.txt \
--do_train \
--do_eval \
--num_train_epochs=100 \
--save_steps=10000 \
--save_total_limit=3 \
--per_device_train_batch_size=1 \
--per_device_eval_batch_size=1 \
--output_dir=model_output \
--use_fast_tokenizer=False
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;以上を実行して学習が終わるまで待つと、作業ディレクトリ内のmodel_outputというディレクトリにモデルが出力されます。学習データは約23000件（約2.5MB）、エポック数100、バッチサイズ1で学習に約4時間かかりました。&lt;/p&gt;
&lt;h2 id="slack-botのデプロイ"&gt;Slack botのデプロイ&lt;/h2&gt;
&lt;p&gt;ここからいよいよConoHa VPSを使います。&lt;/p&gt;
&lt;h3 id="slackアプリの作成"&gt;Slackアプリの作成&lt;/h3&gt;
&lt;p&gt;&lt;a href="https://api.slack.com/" target="_blank" rel="noopener noreferrer"&gt;https://api.slack.com/&lt;/a&gt; よりSlackアプリを作成します。&lt;/p&gt;
&lt;p&gt;今回作りたいSlack botはEvent Subscriptionのbotです。これは、Slack上でメッセージを投稿するなど何かしらの動作をすると、指定したエンドポイントにSlackがリクエストを投げ、そのリクエストに対して何かしらのレスポンスを返すとSlackに反映されるというものです。&lt;/p&gt;
&lt;p&gt;まずはSlack Appを作り、アプリにSlack上の権限を付与します。作り方はこちらの記事を参考にさせていただきました。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://zenn.dev/mokomoka/articles/6d281d27aa344e" target="_blank" rel="noopener noreferrer"&gt;Slack Appの作り方を丁寧に残す【BotとEvent APIの設定編】&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.pci-sol.com/business/service/product/blog/lets-make-slack-app/" target="_blank" rel="noopener noreferrer"&gt;【30分で完成】オウム返しBotから始めるSlackアプリの作り方 | PCIソリューションズ - プロダクト・サービスサイト&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;できたら、適当にSlackのチャンネルを作り、そのチャンネルにAppをインストールします。&lt;/p&gt;
&lt;p&gt;また、このはちゃんとチャットしている雰囲気を出すために、Slack APIのWebサイト上からアイコンを設定しました。アイコンは&lt;a href="https://conoha.mikumo.com/guideline/?btn_id=top--header_guideline" target="_blank" rel="noopener noreferrer"&gt;美雲このはオフィシャルサイト&lt;/a&gt;の二次創作用イラストよりいただきました。&lt;/p&gt;
&lt;h3 id="このはちゃんモデルを組み込んだslack-botのapiのデプロイ"&gt;このはちゃんモデルを組み込んだSlack botのAPIのデプロイ&lt;/h3&gt;
&lt;p&gt;ようやくこのはちゃんbotのデプロイまでたどり着きました。FastAPI (Bolt) + Gunicornを用いて、Slack botのAPIを&lt;code&gt;&amp;lt;VPSのIPアドレス&amp;gt;:8000&lt;/code&gt;に立てることにします。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;実際は、独自ドメインを取ってSSL化した上でSlack APIのURLを&lt;code&gt;https://mydomain.example.com/slack/events&lt;/code&gt;に設定し、NginxでそのURL宛のリクエストを127.0.0.1:8000にリバースプロキシし、APIを127.0.0.1:8000に立てました。ここでは簡単のため独自ドメイン、SSL化、Nginxによるリバースプロキシを使わない前提で説明します。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;まずはConoHa VPS上に使用するcondaの仮想環境を作り、次にFastAPI関連のライブラリと、SlackのEvent Subscription型のアプリを作れるSlack公式のSDKであるBoltというライブラリを入れます。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;$ conda create -n conoha-chatbot python=3.10
$ conda activate conoha-chatbot
$ conda install pip
$ pip install fastapi pydantic uvicorn[standard] gunicorn
$ pip install slack_bolt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;次にPyTorchの環境設定を行います。推論はCPUで行うので、CUDAやCuDNNのインストールは不要です。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;$ conda activate conoha-chatbot
# 前処理で絵文字を削除するのに使う
$ pip install demoji
$ pip install torch torchvision torchaudio --extra-index-url https://download.pytorch.org/whl/cu116
$ pip install pip install sentencepiece datasets evaluate protobuf==3.20
$ pip install git+https://github.com/huggingface/transformers
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;次に、先程訓練したモデルが入ったローカルPCの&amp;quot;model_output&amp;quot;ディレクトリ以下を、VPSの作業ディレクトリ直下に移します。&lt;/p&gt;
&lt;p&gt;そして、以下のmain.pyとgenerate.pyをそれぞれ作業ディレクトリ直下に作成します。&lt;/p&gt;
&lt;p&gt;ディレクトリ構成はこのようになっています。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;$ tree -L 1 &amp;lt;作業ディレクトリ&amp;gt;
&amp;lt;作業ディレクトリ&amp;gt;
├── generate.py
├── main.py
└── model_output
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id="mainpy"&gt;main.py&lt;/h4&gt;
&lt;p&gt;コード内の二つのcredentialはSlack APIのポータルサイトより得られる値を記入します。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;SLACK_BOT_TOKEN
&lt;ul&gt;
&lt;li&gt;左サイドバーの「OAuth &amp;amp; Permissions」ページ内の「Bot User OAuth Token」（xoxb-で始まる文字列）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;SLACK_SIGNING_SECRET
&lt;ul&gt;
&lt;li&gt;左サイドバーの「Basic Information」ページ内の「Signing Secret」&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class="language-python"&gt;from slack_bolt import App
from slack_bolt.adapter.fastapi import SlackRequestHandler
from fastapi import FastAPI, Request
from generate import preprocess, generate
# 自分のcredentialを入れる（コード内に書かず、環境変数として切り出す方が望ましい）
SLACK_BOT_TOKEN = &amp;quot;xoxb-xxxxxx&amp;quot;
SLACK_SIGNING_SECRET = &amp;quot;xxxxxx&amp;quot;
app = App(token=SLACK_BOT_TOKEN, signing_secret=SLACK_SIGNING_SECRET)
app_handler = SlackRequestHandler(app)
# 引数のroot_pathはNginxなどでリバースプロキシするときに変える（今回はルートのまま）
api = FastAPI(root_path=&amp;quot;/&amp;quot;)
@api.post(&amp;quot;/&amp;quot;)
async def endpoint(req: Request):
return await app_handler.handle(req)
# 「Slackにメッセージが投稿されたらこの関数を実行する」という意味のデコレータ
@app.event(&amp;quot;message&amp;quot;)
def handle_app_mentions(body, say, logger):
text = body[&amp;quot;event&amp;quot;][&amp;quot;text&amp;quot;]
res: list[str] = generate(preprocess(text), 1)
res: str = res[0]
print(f&amp;quot;input: {text} - output: {res}&amp;quot;)
say(res)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id="generatepy"&gt;generate.py&lt;/h4&gt;
&lt;pre&gt;&lt;code class="language-python"&gt;import re
import unicodedata
import demoji
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
device = torch.device(&amp;quot;cpu&amp;quot;)
tokenizer = AutoTokenizer.from_pretrained(&amp;quot;rinna/japanese-gpt2-small&amp;quot;)
tokenizer.do_lower_case = True
model = AutoModelForCausalLM.from_pretrained(&amp;quot;&amp;lt;作業ディレクトリ&amp;gt;/model_output/&amp;quot;)
model.to(device)
def preprocess(text: str) -&amp;gt; str:
&amp;quot;&amp;quot;&amp;quot;
テキストを前処理する
&amp;quot;&amp;quot;&amp;quot;
# windowsの全角チルダを波ダッシュに変換する（いわゆる全角チルダ・波ダッシュ問題）
text = re.sub(&amp;quot;\uff5e&amp;quot;, &amp;quot;\u301c&amp;quot;, text)
# 絵文字を削除
text = demoji.replace(text, &amp;quot;&amp;quot;)
text = unicodedata.normalize(&amp;quot;NFKC&amp;quot;, text)
# 顔文字を雑に削除
text = re.sub(r&amp;quot;[\(（].*[\)）]&amp;quot;, &amp;quot;&amp;quot;, text)
# URLを削除
text = re.sub(r&amp;quot;https?://[\w/:%#\$&amp;amp;\?\(\)~\.=\+\-]+&amp;quot;, &amp;quot;&amp;quot;, text)
# ハッシュタグを削除
text = re.sub(r&amp;quot;#.+ ?&amp;quot;, &amp;quot;&amp;quot;, text)
# 表記ゆれ系を統一
text = re.sub(r&amp;quot;[・、。]{2,3}&amp;quot;, &amp;quot;…&amp;quot;, text)
text = re.sub(r&amp;quot;\.\.\.&amp;quot;, &amp;quot;…&amp;quot;, text)
text = re.sub(&amp;quot;ー{2,}&amp;quot;, &amp;quot;ー&amp;quot;, text)
text = re.sub(r&amp;quot;!{2,}&amp;quot;, &amp;quot;!&amp;quot;, text)
text = re.sub(r&amp;quot;\?{2,}&amp;quot;, &amp;quot;?&amp;quot;, text)
text = re.sub(r&amp;quot;…{2,}&amp;quot;, &amp;quot;…&amp;quot;, text)
text = text.strip()
return text
def generate(input: str, num: int = 1) -&amp;gt; list[str]:
&amp;quot;&amp;quot;&amp;quot;
推論する
引数inputのテキストからnum個のテキストを作る
&amp;quot;&amp;quot;&amp;quot;
input_text = &amp;quot;&amp;lt;s&amp;gt;&amp;quot; + input + &amp;quot;[SEP]&amp;quot;
input_ids = tokenizer.encode(input_text, return_tensors=&amp;quot;pt&amp;quot;).to(device)
# ここのパラメータを変えると出力される文章が変わる
out = model.generate(
input_ids, do_sample=True, top_p=0.95, top_k=500, repetition_penalty=1.2,
num_return_sequences=num, max_length=30, bad_words_ids=[[1], [5]]
)
res = []
for output_text in tokenizer.batch_decode(out):
output_text = output_text.split(&amp;quot;[SEP]&amp;lt;/s&amp;gt;&amp;quot;)[1]
output_text = output_text.replace(&amp;quot;&amp;lt;/s&amp;gt;&amp;quot;, &amp;quot;&amp;quot;)
res.append(output_text)
return res
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id="技術的な説明"&gt;技術的な説明&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;APIの起動時にこのはちゃんモデルがロードされます。&lt;/li&gt;
&lt;li&gt;Slack Appをインストールしたチャンネルで何らかのメッセージを入力すると、そのメッセージを含むJSONがこのAPIにPOSTされます。&lt;/li&gt;
&lt;li&gt;API側では以下の処理が行われます。
&lt;ul&gt;
&lt;li&gt;実際にSlackに入力されたテキストをJSONから取り出します。&lt;/li&gt;
&lt;li&gt;それをテキストの前処理関数である&lt;code&gt;preprocess&lt;/code&gt;で前処理します。
&lt;ul&gt;
&lt;li&gt;ローカルPCでの前処理の際に使用した関数と同じものです。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;モデルに通すために、前処理した入力するテキストを&lt;code&gt;&amp;lt;s&amp;gt;（入力するテキスト）[SEP]&lt;/code&gt;の形の文字列にします。&lt;/li&gt;
&lt;li&gt;これをこのはちゃんモデルに通し、出力の文字列を得ます。&lt;/li&gt;
&lt;li&gt;Slack側にレスポンスを返します。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Slackにメッセージが投稿されたらそれを受け取って何かしらのレスポンスを返すという処理は、Boltを使わずrequestsなどを使って自分で一から作ることもできますが、結構骨が折れます。Boltはデコレータによってこの処理を簡単に記述できるライブラリです。BoltにはHTTPServerアダプタが組み込まれているためBolt単体でもAPIを立ち上げられますが、FastAPIやFlaskのようなAPIのライブラリにBoltを組み込むことができます（&lt;a href="https://slack.dev/bolt-python/ja-jp/concepts#adapters" target="_blank" rel="noopener noreferrer"&gt;公式のドキュメントでは、本番環境ではそうすることが推奨されています&lt;/a&gt;）。BoltのFastAPIへの組み込み方については、&lt;a href="https://github.com/slackapi/bolt-python/blob/main/examples/fastapi/app.py" target="_blank" rel="noopener noreferrer"&gt;BoltのGitHubライブラリ内のサンプルコード&lt;/a&gt;を参考にしました。&lt;/p&gt;
&lt;p&gt;テキストを与えると返事を出力するGPT-2の推論部分を別のAPIとして作成し、Slack botのAPIではそのAPIを叩きに行くのがよくある構成だと思いますが、簡単のためbotのAPI内で直接モデルをロードすることにしました。&lt;/p&gt;
&lt;h4 id="apiのデプロイ"&gt;APIのデプロイ&lt;/h4&gt;
&lt;p&gt;アプリケーションサーバにGunicornを用いて、このAPIを8000番ポートで公開します。事前にファイアウォールで8000番ポートを開けておきます。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;$ cd &amp;lt;作業ディレクトリ&amp;gt;
$ python -m gunicorn main:app --bind 0:0:0:0:8000 -w 1 -k uvicorn.workers.UvicornWorker
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Gunicornのワーカー数（&lt;code&gt;-w 1&lt;/code&gt;の部分）は1にしています。各ワーカーでこのはちゃんモデルがロードされるため、メモリ2GBのプランではメモリ使用量的にワーカー数は1がギリギリでした。なお、VPS（CPU3コア、メモリ2GB）にssh接続した状態でメモリ使用量（&lt;code&gt;sar -r&lt;/code&gt;コマンドの&lt;code&gt;%memused&lt;/code&gt;）を確認してみると、APIの起動前は10%、起動直後（モデルをロードしているとき）は50%、メッセージ待機時と推論時は40%程度を推移していました。&lt;/p&gt;
&lt;h3 id="apiエンドポイントをslackに登録する"&gt;APIエンドポイントをSlackに登録する&lt;/h3&gt;
&lt;p&gt;先程Slack Appを作成したSlack APIのWebサイトより、左サイドバーの&amp;quot;Event Subscriptions&amp;quot;を開きます。画像の&amp;quot;Enable Events&amp;quot;の横のトグルをOnにした後、今立ち上げたAPIエンドポイントのURLをRequest URLの欄に入力します。&lt;/p&gt;
&lt;p&gt;画像内の&amp;quot;Request URL&amp;quot;に&lt;code&gt;http://&amp;lt;VPSのIPアドレス&amp;gt;:8000&lt;/code&gt;を入力します。&lt;/p&gt;
&lt;p&gt;&lt;img src="./images/slack_event_subscription.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;入力するとSlack側から立ち上げたAPIにドメインの所有権を確かめるためのPOSTが行われます。上手くAPIが立ち上げられていれば、bolt側でこれを打ち返してくれるので、&amp;ldquo;Verified&amp;quot;と表示されるはずです。&lt;/p&gt;
&lt;p&gt;最後にSlack Appをインストールしたチャンネルに適当にメッセージを入力してみて、数秒経ってからこのはちゃんbotから返事が来れば成功です。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;APIのURLの初回登録時は、ドメインの所有権を確認するために、SlackからPOSTされるjsonの&amp;quot;challenge&amp;quot;というキーの値を送り返す必要があります（画像の&amp;quot;We&amp;rsquo;ll send HTTP POST requests to this URL when events occur.（以下省略）&amp;ldquo;に書いてある通り）。上記で利用したBoltでAPIを立てるとこの対応を内部で行ってくれますので、この処理に関するコードを書く必要はありません。
Boltを用いない場合はFastAPIなどのAPIフレームワークを使って自分でAPIを立てて対応する必要があります。詳細は&lt;a href="https://api.slack.com/apis/connections/events-api#the-events-api__subscribing-to-event-types__events-api-request-urls__request-url-configuration--verification" target="_blank" rel="noopener noreferrer"&gt;Slackの公式ドキュメント&lt;/a&gt;をご参照ください。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="会話してみた"&gt;会話してみた&lt;/h2&gt;
&lt;p&gt;会話してみます。&lt;/p&gt;
&lt;p&gt;おはようと挨拶するとちゃんとおはようと返してくれます。ちなみに、このはちゃんモデルは文脈は考慮しません。（前のやり取りを踏まえて次の出力の文章が変わるということはありません）&lt;/p&gt;
&lt;p&gt;&lt;img src="./images/chat01.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;こんばんはと挨拶してもおはようとしか返してくれません。謎の冬季限定チョコレート推し…。&lt;/p&gt;
&lt;p&gt;&lt;img src="./images/chat02.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;清楚かわいいとほめると喜んでくれます。&lt;/p&gt;
&lt;p&gt;&lt;img src="./images/chat03.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;あんずちゃんにはたまに厳しくなるみたいです。&lt;/p&gt;
&lt;p&gt;&lt;img src="./images/chat04.png" alt=""&gt;
&lt;img src="./images/chat05.png" alt=""&gt;&lt;/p&gt;
&lt;h2 id="おわりに"&gt;おわりに&lt;/h2&gt;
&lt;p&gt;雰囲気は何となくこのはちゃんっぽい感じがしますね。個人的には満足しましたが、意味が通っていない返事をすることも結構ありました。前処理の改善やよりパラメータ数の大きい事前学習モデルの使用、パラメータチューニングなどが今後の課題でしょうか。&lt;/p&gt;
&lt;p&gt;以上、ConoHa VPSでAPIを立てて深層学習チャットボットを作ることができました。今後もConoHaで物を作っていきたいです。&lt;/p&gt;
&lt;h2 id="参考"&gt;参考&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;公式ドキュメントなど
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://twitter.com/MikumoConoHa" target="_blank" rel="noopener noreferrer"&gt;Twitter: @MikumoConoHa&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://conoha.mikumo.com/" target="_blank" rel="noopener noreferrer"&gt;美雲このはオフィシャルサイト&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;本記事の中で使用したこのはちゃんのイラストはこちらからいただきました。&lt;/li&gt;
&lt;li&gt;©GMO Internet Group, Inc., 再利用禁止です。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.rinna.jp/" target="_blank" rel="noopener noreferrer"&gt;りんなオフィシャルサイト&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/rinnakk/japanese-pretrained-models" target="_blank" rel="noopener noreferrer"&gt;rinnakk/japanese-pretrained-models&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;rinna社による日本語GPT-2の事前学習モデル&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://slack.dev/bolt-python/ja-jp/tutorial/getting-started" target="_blank" rel="noopener noreferrer"&gt;Slack | Bolt for Python&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/slackapi/bolt-python/blob/main/examples/fastapi/app.py" target="_blank" rel="noopener noreferrer"&gt;BoltのGitHubライブラリ内のサンプルコード&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://api.slack.com/apis/connections/events-api#the-events-api__subscribing-to-event-types__events-api-request-urls__request-url-configuration--verification" target="_blank" rel="noopener noreferrer"&gt;Using the Slack Events API | Slack&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;参考にさせていただいたサイト
&lt;ul&gt;
&lt;li&gt;WindowsにおけるPyTorchのGPU環境の作り方
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://zenn.dev/opamp/articles/c5e200c6b75912" target="_blank" rel="noopener noreferrer"&gt;Windows10にPyTorch1.10とCUDA11.3の環境を作る&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;rinnakk/japanese-pretrained-modelsの使い方
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://qiita.com/m__k/items/36875fedf8ad1842b729#%E3%83%95%E3%82%A1%E3%82%A4%E3%83%B3%E3%83%81%E3%83%A5%E3%83%BC%E3%83%8B%E3%83%B3%E3%82%B0" target="_blank" rel="noopener noreferrer"&gt;GPT-2をファインチューニングしてニュース記事のタイトルを条件付きで生成してみた。 - Qiita&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Slack Appの作り方
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://zenn.dev/mokomoka/articles/6d281d27aa344e" target="_blank" rel="noopener noreferrer"&gt;Slack Appの作り方を丁寧に残す【BotとEvent APIの設定編】&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.pci-sol.com/business/service/product/blog/lets-make-slack-app/" target="_blank" rel="noopener noreferrer"&gt;【30分で完成】オウム返しBotから始めるSlackアプリの作り方 | PCIソリューションズ - プロダクト・サービスサイト&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>代替文字を含むShift-JISのテキストファイルをRで読み込む（おまけにPythonも）</title><link>https://suzunano.net/posts/read-mojibake-text/</link><pubDate>Mon, 21 Nov 2022 00:00:00 +0900</pubDate><guid>https://suzunano.net/posts/read-mojibake-text/</guid><description>&lt;h2 id="概要"&gt;概要&lt;/h2&gt;
&lt;h3 id="question"&gt;Question&lt;/h3&gt;
&lt;p&gt;以下の&lt;code&gt;data.txt&lt;/code&gt;というファイル名のShift-JISのテキストファイルを考えます。ただし、代替文字（Replacement Character, Unicodeで&lt;code&gt;U+FFFD&lt;/code&gt;）が含まれる行が存在する可能性があります。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-text"&gt;りんご
みかん
バナナ
（以下略）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;いま、このテキストファイルを、代替文字は削除したうえで1行が1要素の文字列ベクトル（&lt;code&gt;c(&amp;quot;りんご&amp;quot;, &amp;quot;みかん&amp;quot;, &amp;quot;バナナ&amp;quot;...)&lt;/code&gt;）として読み込みたいです。ただし、環境はWindows, RはR&amp;gt;=4.2.0のバージョンとします。&lt;/p&gt;
&lt;h3 id="answer"&gt;Answer&lt;/h3&gt;
&lt;p&gt;回答は一例です。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-r"&gt;readr::read_lines_raw(&amp;quot;data.txt&amp;quot;) %&amp;gt;%
stringi::stri_encode(from=&amp;quot;Shift-JIS&amp;quot;) %&amp;gt;%
stringr::str_remove_all(&amp;quot;\ufffd&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id="環境"&gt;環境&lt;/h3&gt;
&lt;pre&gt;&lt;code class="language-text"&gt;version R version 4.2.1 (2022-06-23 ucrt)
os Windows 10 x64 (build 19045)
system x86_64, mingw32
ui RStudio
language (EN)
collate Japanese_Japan.utf8
ctype Japanese_Japan.utf8
tz Asia/Tokyo
date 2022-11-21
rstudio 2022.07.1+554 Spotted Wakerobin (desktop)
（一部割愛）
readr: 2.1.3
stringi: 1.7.8
stringr: 1.4.1
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id="説明"&gt;説明&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;data.txt&lt;/code&gt;に代替文字が含まれていなければ、以下で問題ありません。代替文字を含む場合でも、ファイルがUTF-8やEUC-JPの場合はencodingをそれに変えれば上記の環境で同様に問題なく読み込めました。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-r"&gt;readr::read_lines(&amp;quot;data.txt&amp;quot;, locale=readr::locale(encoding=&amp;quot;Shift-JIS&amp;quot;))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;しかし、代替文字を含むShift-JISのファイルの場合、&lt;code&gt;Error: Invalid Multibyte Sequence&lt;/code&gt;というエラーが出てRStudioがクラッシュします。そのため、一旦raw形式で読み込んでから文字列に直します。raw形式で読み込む関数は、例えば&lt;code&gt;readr::read_lines_raw&lt;/code&gt;があります。&lt;/p&gt;
&lt;p&gt;次に、raw形式から文字列に変換する必要がありますが、&lt;code&gt;rawToChar&lt;/code&gt;を用いると文字化けしてしまいます。理由は私にはよく分かっていないのですが、こちらのStack Overflowのアンサーによると（&lt;a href="https://stackoverflow.com/questions/33068063/encoding-and-raw-in-r" target="_blank" rel="noopener noreferrer"&gt;Encoding and raw in R - Stack Overflow&lt;/a&gt;）、&lt;code&gt;charToRaw&lt;/code&gt;の関数ヘルプにはエンコーディングを考慮しないと記載があるので、&lt;code&gt;rawToChar&lt;/code&gt;も同様にエンコーディングが考慮されないのではないか、とのことです。R&amp;gt;=4.2.0の環境では日本語環境のWindowsでも文字のロケールがUTF-8ですので、辻褄が合います。&lt;/p&gt;
&lt;p&gt;したがって、エンコーディングを考慮してrawから文字列に変換する&lt;code&gt;stringi::stri_encode&lt;/code&gt;を用います。&lt;/p&gt;
&lt;p&gt;最後に&lt;code&gt;stringr::str_remove_all&lt;/code&gt;で代替文字である&lt;code&gt;\ufffd&lt;/code&gt;を削除すれば完成です。&lt;/p&gt;
&lt;h2 id="python版"&gt;Python版&lt;/h2&gt;
&lt;p&gt;おまけにPythonで同じことをするコードを載せておきます。バイナリモードで1行ずつ読み込んでShift-JISに変換すればOKです。&lt;code&gt;line_binary.decode&lt;/code&gt;で&lt;code&gt;ignore&lt;/code&gt;とすればReplacement Characterを読み込んだ上で削除されますが、ここを&lt;code&gt;replace&lt;/code&gt;としてから&lt;code&gt;re.sub&lt;/code&gt;で削除したり他の文字に置き換えることも可能です。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-python"&gt;res = []
with open(&amp;quot;data.txt&amp;quot;, &amp;quot;rb&amp;quot;) as f:
while True:
line_binary = f.readline()
line = line_binary.decode(&amp;quot;Shift-JIS&amp;quot;, &amp;quot;ignore&amp;quot;)
if line == &amp;quot;&amp;quot;:
break
res.append(line)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id="参考にさせていただいたサイト"&gt;参考にさせていただいたサイト&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://stackoverflow.com/questions/33068063/encoding-and-raw-in-r" target="_blank" rel="noopener noreferrer"&gt;Encoding and raw in R - Stack Overflow&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://qiita.com/hakua-doublemoon/items/eb717c2b67c033322adb" target="_blank" rel="noopener noreferrer"&gt;Pythonと壊れたテキスト（UnicodeDecodeError） - Qiita&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>スマホゲーム「CUE!」のストーリーをOpenCVとOCR（Vision API）で書き起こす</title><link>https://suzunano.net/posts/cue-ocr/</link><pubDate>Wed, 05 Oct 2022 00:00:00 +0900</pubDate><guid>https://suzunano.net/posts/cue-ocr/</guid><description>&lt;h2 id="概要"&gt;概要&lt;/h2&gt;
&lt;h3 id="はじめに"&gt;はじめに&lt;/h3&gt;
&lt;p&gt;次世代声優育成スマホゲーム&lt;a href="https://www.cue-liber.jp/" target="_blank" rel="noopener noreferrer"&gt;CUE!&lt;/a&gt;のゲームシナリオをOpenCVとOCR（Google
CloudのVision API）を使って自動で書き起こしてみました。&lt;/p&gt;
&lt;p&gt;プレイヤーが声優事務所のマネージャーとなり、事務所に所属する16人の新人声優を育てていくというゲームです。&lt;/p&gt;
&lt;div align="center"&gt;
&lt;img src="image.jpg" width="600px"&gt;
&lt;/div&gt;
&lt;p&gt;美晴さんはほんわかおっとりしていながらも、周りの子たちをよく見ていて支えになるお姉さんです。&lt;/p&gt;
&lt;p&gt;CUE!にはシナリオパートがあり、キャラクター同士やキャラクターとプレイヤーの掛け合いを見ることができます。CUE!ではシナリオは上に挙げたような画像で、画像下部の台詞がテロップのように流れながらキャラクターがしゃべります。&lt;/p&gt;
&lt;p&gt;サービス開始日から遊んでいたアプリだったので、セリフを使って何か分析したりモデルを組んだりしたいと思ったのですが、そもそもセリフのテキストデータがないため自分でセリフを書き起こしてデータセットを作る必要がありました。&lt;/p&gt;
&lt;p&gt;セリフのスクリーンショットを用意しなくとも、スマートフォンやタブレットでストーリーを流しっぱなしにしたまま画面を録画した動画をインプットにできると楽です。そのためセリフ画像を切り出す所もコードで対応することにしました。&lt;/p&gt;
&lt;p&gt;CUE!はもうサービスが終了してしまったゲームであり、手元にはサービス提供中にストーリーを撮った動画が残っているという事情もあります。&lt;/p&gt;
&lt;h3 id="やったこと"&gt;やったこと&lt;/h3&gt;
&lt;p&gt;CUE!のストーリーを録画した動画のmp4ファイルをインプットとし、各セリフの発話キャラクターとセリフ内容を列に、セリフの数だけ行を持つようなcsvファイルを出力しました。&lt;/p&gt;
&lt;p&gt;インプットに使用する動画はストーリーごとに1本ずつ分かれた動画であり、スマートフォンかタブレットの画面録画か、あるいはキャプチャボードを用いてスマートフォンやタブレットを接続したPCから録画するかのどちらかを想定しています。&lt;/p&gt;
&lt;h3 id="技術構成"&gt;技術構成&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Step1: 動画から静止画を切り出す
&lt;ul&gt;
&lt;li&gt;FFmpeg&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Step2:
切り出した画像から、セリフが載っている画像だけを漏れなくダブりなく取り出す
&lt;ul&gt;
&lt;li&gt;セリフはテロップのように流れるので、Step1で切り出した画像には、キャラクターが話し終わる途中でセリフが切れてしまっている画像が含まれていたり、セリフが全て写っている画像がダブっていたりします。また、セリフが含まれていない画像もあります。これらを取り除きます。&lt;/li&gt;
&lt;li&gt;Python + OpenCV&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Step3: 残った画像について、超解像で画像を拡大する
&lt;ul&gt;
&lt;li&gt;waifu2x-caffe&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Step4: 前処理する
&lt;ul&gt;
&lt;li&gt;OpenCVの画像処理ではバイラテラルフィルタと収縮を使用&lt;/li&gt;
&lt;li&gt;Python + OpenCV&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Step5: OCRでテキストを取り出す
&lt;ul&gt;
&lt;li&gt;Python + Google Cloud Vision API&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;CUE!以外のスマホゲームでも同様のロジックで文字起こしができると思います。（ただし画像処理部分のコードはゲームに応じて描き直す必要があります）&lt;/p&gt;
&lt;h3 id="環境"&gt;環境&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;ハード
&lt;ul&gt;
&lt;li&gt;Core i9-9900K&lt;/li&gt;
&lt;li&gt;NVIDIA GeForce RTX 2060 Super&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;ソフト
&lt;ul&gt;
&lt;li&gt;Windows10&lt;/li&gt;
&lt;li&gt;Python 3.10.0&lt;/li&gt;
&lt;li&gt;opencv-python 4.5.5.64&lt;/li&gt;
&lt;li&gt;waifu2x-caffe 1.2.0.4&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="step1-動画から静止画を切り出す"&gt;Step1 動画から静止画を切り出す&lt;/h2&gt;
&lt;p&gt;FFmpegをコマンドラインから使えるようにダウンロードして設定した後、以下をコマンドプロンプトかbashで実行すると動画1秒につき15枚のjpg画像が切り出されます。&lt;/p&gt;
&lt;p&gt;1時間の動画であれば15×60×60=54000枚の画像が出力されます。動画の解像度や画質にもよりますが、画像1枚で数百KB程度になりますので、ローカルのストレージに十分な容量を確保する必要があります。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;# 切り出した静止画を保存するフォルダは、事前に作成しておくこと
cd 切り出した静止画を保存するフォルダパス
# `-q:v 1`は最高画質で保存する
# image_{7桁の連番}.jpgで保存される
ffmpeg -i &amp;quot;動画のファイルパス&amp;quot; -r 15 -q:v 1 image_%07d.jpg -vcodec jpg
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id="step2-画像を漏れなくダブりなく取り出す"&gt;Step2 画像を漏れなくダブりなく取り出す&lt;/h2&gt;
&lt;h3 id="課題"&gt;課題&lt;/h3&gt;
&lt;p&gt;Step1で静止画を切り出すことができましたが、得られたキャプチャ画像には問題が3つあります。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;セリフが表示されていない幕間の場面をキャプチャしている&lt;/li&gt;
&lt;li&gt;セリフが全て表示し終わる前にキャプチャしているため、セリフが途中で切れている&lt;/li&gt;
&lt;li&gt;セリフが全て表示し終わってからキャプチャしているが、同じセリフが写っているキャプチャが何枚もある&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;1と2の画像は全て削除して、3のキャプチャはセリフごとに1枚だけ残したいです。&lt;/p&gt;
&lt;p&gt;参考: 2の例&lt;/p&gt;
&lt;div align="center"&gt;
&lt;img src="image_split.jpg" width="600px"&gt;
&lt;/div&gt;
&lt;h3 id="解決策"&gt;解決策&lt;/h3&gt;
&lt;p&gt;まず問題1についてですが、画像を見て分かるように、セリフが映っている画像では、セリフの長方形の領域の背景はほぼ白です。一方で、セリフが表示されていない画像はそうではありません。&lt;/p&gt;
&lt;p&gt;このことを活かし、まずセリフの領域を切り出し、とりあえず雑にセリフの領域の左上の隅からx軸方向に2px、y軸方向に2pxの1ピクセルと、右下の隅からx軸方向に-2px、y軸方向に-2pxの1ピクセルの2点を取り出し、この2点がいずれも白色でなければその画像にはセリフのボックスが含まれていないとみなしてその画像を削除することにしました。&lt;/p&gt;
&lt;p&gt;セリフのボックスの位置は全ての画像で固定です。よって、GIMPなどのマウスカーソルを載せた場所の座標を取得できる画像ビューアを用いて事前にボックスの四隅の座標を調べておき、その座標を指定することで元の画像からセリフのボックスを切り出すことができます。&lt;/p&gt;
&lt;p&gt;白色かどうかの判定ですが、真っ白を表す(R, G, B) = (255, 255,
255)と比較してCIEDE2000の色差が5以上であれば白色ではないとみなすことにしました。CIEDE2000での色差は、&lt;code&gt;skimage.color.deltaE_ciede2000&lt;/code&gt;で求めることができます。&lt;/p&gt;
&lt;p&gt;次に問題2と3の解決方法についてです。&lt;/p&gt;
&lt;p&gt;今、セリフの領域を切り出して二値化した画像を用意し、この画像内で「最も右側の黒色の画素（＝行列値が0）のx座標」を考えてみます。ただしセリフは最大で2行あるため、セリフのボックスを上下2分割し、下段のセリフを上段のセリフの右端にくっつけた画像で考えます。&lt;/p&gt;
&lt;p&gt;元々のセリフのボックスの領域はこれですが、&lt;/p&gt;
&lt;p&gt;&lt;img src="cue_text_ocr_files/figure-gfm/cell-3-output-1.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;大津の二値化を行ってから上下を横にくっつけたこちらの画像を用いて考えます。&lt;/p&gt;
&lt;p&gt;&lt;img src="cue_text_ocr_files/figure-gfm/cell-4-output-1.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;以下、画像iの「最も右側の黒色のピクセルのx座標」を$x_{i}$と表します。ただし、iは動画の初めから終わりまで順番に並んでいるものとします。&lt;/p&gt;
&lt;p&gt;$x_{i}$を求める関数は下のような感じで書けます。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-python"&gt;def calc_max_serifu_px(img: np.ndarray):
# x = x' (0 &amp;lt;= x' &amp;lt;= img.shape[1]) の直線上に0である画素が2点以上あれば、
# x = x'には黒色の画素があるとみなす
# （1点だけだとノイズの可能性があるため2点とした）
idx_text_pixel = (np.sum(img == 0, axis=0) &amp;gt;= 2).astype(np.int16)
idx = 0 if np.all(idx_text_pixel == 0) else np.where(idx_text_pixel == 1)[0][-1]
return idx
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class="language-python"&gt;calc_max_serifu_px(new_img_text)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;1573
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一つ前の画像を見ると、確かにx座標が1570px程度の所までセリフがあります。&lt;/p&gt;
&lt;p&gt;先程の2の状態では、セリフはテロップのように流れるため、$x_{i}$は広義の単調増加（単調非減少）です。3の状態に移ると、セリフは全て流れ切っているので$x_{i}$は横ばいです。次のセリフの2の状態に移ると、$x_{i}$はその前の$x_{i-1}$と比べ、大きく変動します。そこから再び$x_{i}$は広義の単調増加となります。これが繰り返されます。&lt;/p&gt;
&lt;p&gt;いま取り出したい「ユニークなセリフの画像」は、次の台詞の2の状態に移る直前の3の状態の画像ですから、すなわち以下の2つの条件を満たすiを見つければよいということになります。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$|x_{i-j}-x_{i-(j-1)}|, |x_{i-(j-1)}-x_{i-(j-2)}|, \dots, |x_{i-1}-x_{i}|$の平均が$a$以下（$a$は$a \geq 0$の定数）
&lt;ul&gt;
&lt;li&gt;すなわち、$x_{i-j}, x_{i-(j-1)}, \dots, x_{i}$は横ばいということ&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;$|x_{i}-x_{i+1}| \geq b$（$b$は$b \geq 0$の定数）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;j, a,
bは事前に決める必要がありますが、色々試した結果$j = 4, a = 2, b = 5$としました。この数値はStep1で動画から静止画を取り出す際の、1秒あたり何枚の画像を取り出すかに影響を受けます。&lt;/p&gt;
&lt;p&gt;以上2つのロジックによって、セリフが映っていない画像は削除した上で、ユニークなセリフの画像のみを取り出すことができます。ストーリーを収録したインプットの動画を1時間とすると、Step1で得られた54000枚の画像から、Step2で500枚程度まで減らすことができました。&lt;/p&gt;
&lt;h2 id="step3-画像を超解像で拡大する"&gt;Step3 画像を超解像で拡大する&lt;/h2&gt;
&lt;p&gt;Step2で残った画像全てについて、以降のステップでOCRで画像を読み取りますが、その前に超解像でノイズ除去・拡大し、OpenCVでさらにノイズ除去などの前処理を行う必要があります。Step3は前者、Step4は後者です。&lt;/p&gt;
&lt;p&gt;超解像というとOpenCVの&lt;code&gt;cv2.dnn_superres&lt;/code&gt;を使う手もありますが、waifu2xのcaffe実装である&lt;a href="https://github.com/lltcggie/waifu2x-caffe" target="_blank" rel="noopener noreferrer"&gt;lltcggie
/
waifu2x-caffe&lt;/a&gt;を用いました。&lt;/p&gt;
&lt;p&gt;上のリンクからダウンロード後GUI版を起動し、Step2までで取り出された画像を超解像にかけます。設定値は下記の通りです。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;出力拡張子: jpg&lt;/li&gt;
&lt;li&gt;出力画質: 95&lt;/li&gt;
&lt;li&gt;変換モード: ノイズ除去（自動判別）と拡大&lt;/li&gt;
&lt;li&gt;ノイズ除去レベル: レベル3&lt;/li&gt;
&lt;li&gt;拡大サイズ: 拡大率で指定（2.0）&lt;/li&gt;
&lt;li&gt;モデル: 2次元イラスト（UpRGBモデル）（TTAモードを使わない）&lt;/li&gt;
&lt;li&gt;分割サイズ: 128, バッチサイズ: 8&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;ノイズを除去したうえで縦横2倍に拡大しました。GPUを使って5万枚の画像を4時間程度で処理できました。&lt;/p&gt;
&lt;h2 id="step4-前処理する"&gt;Step4 前処理する&lt;/h2&gt;
&lt;pre&gt;&lt;code class="language-python"&gt;import cv2
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from google.cloud import vision
from google.oauth2 import service_account
# 表示用の関数
def view(img: np.ndarray) -&amp;gt; None:
plt.imshow(cv2.cvtColor(img,cv2.COLOR_BGR2RGB))
img = cv2.imread(&amp;quot;image_waifu2x.jpg&amp;quot;)
view(img)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src="cue_text_ocr_files/figure-gfm/cell-7-output-1.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;この画像から名前の部分とセリフ部分を切り出します。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-python"&gt;# 画像上にマウスポインタを載せるとその場所の座標を
# 取得できる画像ビューア（GIMPなど）を使用し、適当な座標を調べます
name_topleft = [155*2, 550*2]
name_bottomright = [340*2, 580*2]
text_topleft = [155*2, 595*2]
text_bottomright = [1085*2, 670*2]
name_x1, name_y1 = name_topleft[0], name_topleft[1]
name_x2, name_y2 = name_bottomright[0], name_bottomright[1]
text_x1, text_y1 = text_topleft[0], text_topleft[1]
text_x2, text_y2 = text_bottomright[0], text_bottomright[1]
img_name = img[name_y1:name_y2, name_x1:name_x2]
img_text = img[text_y1:text_y2, text_x1:text_x2]
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class="language-python"&gt;view(img_name)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src="cue_text_ocr_files/figure-gfm/cell-9-output-1.png" alt=""&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-python"&gt;view(img_text)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src="cue_text_ocr_files/figure-gfm/cell-10-output-1.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;名前部分とセリフ部分にバイラテラルフィルタ -&amp;gt; 二値化 -&amp;gt;
収縮をかけます。&lt;/p&gt;
&lt;p&gt;文字の縁のノイズを除去するためにバイラテラルフィルタをかけるとともに、文字が比較的太く、線と線がつながりやすく見えるため、収縮をかけて線同士を離します。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-python"&gt;def preprocess_img_name(img: np.ndarray) -&amp;gt; np.ndarray:
# パラメータは適当（目視でよさそうなパラメータを適当に採用した）
img = cv2.bilateralFilter(img, d=10, sigmaColor=20, sigmaSpace=20)
img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
_, img = cv2.threshold(img, 0, 255, cv2.THRESH_OTSU)
# パラメータは適当
img = cv2.erode(img, kernel=np.ones((2, 2), np.uint8),iterations=1)
img = cv2.bitwise_not(img)
return img
def preprocess_img_text(img: np.ndarray) -&amp;gt; np.ndarray:
img = cv2.bilateralFilter(img, d=10, sigmaColor=20, sigmaSpace=20)
img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
img = cv2.bitwise_not(img)
_, img = cv2.threshold(img, 0, 255, cv2.THRESH_OTSU)
img = cv2.erode(img, kernel=np.ones((2, 2), np.uint8), iterations=1)
img = cv2.bitwise_not(img)
return img
img_name_preprocessed = preprocess_img_name(img_name)
img_text_preprocessed = preprocess_img_text(img_text)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;名前部分とセリフ部分の横幅を揃えて、一枚の画像に結合します。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-python"&gt;img_name_preprocessed = cv2.copyMakeBorder(img_name_preprocessed, 10, 10, name_x1 - text_x1, text_x2-name_x2, cv2.BORDER_CONSTANT, value=(255, 255, 255))
img_text_preprocessed = cv2.copyMakeBorder(img_text_preprocessed, 10, 10, 0, 0, cv2.BORDER_CONSTANT, value=(255, 255, 255))
img_preprocessed = cv2.vconcat([img_name_preprocessed, img_text_preprocessed])
view(img_preprocessed)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src="cue_text_ocr_files/figure-gfm/cell-12-output-1.png" alt=""&gt;&lt;/p&gt;
&lt;h2 id="step5-ocrにかける"&gt;Step5 OCRにかける&lt;/h2&gt;
&lt;p&gt;ここまで前処理した画像を、Vision
APIを用いてOCRにかけます。月間1000コールまで無料、それ以上は500万コールまでは1000コールごとに1.5ドルとリーズナブルです。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-python"&gt;class VisionApi:
def __init__(self, credential_path: str) -&amp;gt; None:
self.credentials = service_account.Credentials.from_service_account_file(credential_path)
self.client = vision.ImageAnnotatorClient(credentials=self.credentials)
def ocr(self, img: np.ndarray) -&amp;gt; str:
content = cv2.imencode(&amp;quot;.png&amp;quot;, img)[1].tobytes()
vision_img = vision.Image(content=content)
response = self.client.document_text_detection(image=vision_img)
text = response.full_text_annotation.text
return text
# このcredentialのjsonファイルはGCPに登録するとダウンロードできます
va = VisionApi(&amp;quot;../python-ocr.json&amp;quot;)
texts = va.ocr(img_preprocessed)
texts
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;'夜峰美晴\nわたし達の声を通して、素敵な風を吹かせられたらいいなって･･･\nそれが、わたし達の進む道･･・・･･、 なんじゃないかな?'
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;“名前（改行記号）セリフ1行目（改行記号）セリフ2行目”の文字列で返ってきていることを利用し、名前とセリフに分けてpandas.DataFrameにします。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-python"&gt;def parse_to_df(texts: list[str]) -&amp;gt; pd.DataFrame:
texts = [i for i in texts.splitlines()]
if len(texts) == 0:
name = &amp;quot;&amp;quot;
line = &amp;quot;&amp;quot;
elif len(texts) == 1:
name = texts[0]
line = &amp;quot;&amp;quot;
else:
name, line = texts[0], texts[1:]
df = pd.DataFrame([{&amp;quot;name&amp;quot;: name, &amp;quot;line&amp;quot;: line}])
return df
df = parse_to_df(texts)
res = (
df
# たまにセリフがない画像があるので、セリフがない場合は除外する
.loc[lambda d: ~pd.isna(d.line)]
# line列はlist（行が1行だけなら要素数は1、2行なら2）なので、改行を[br]で繋ぐ
.assign(line_joined = lambda d: d.line.map(lambda x: &amp;quot;[br]&amp;quot;.join(x)))
)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class="language-python"&gt;print(res.name.to_list())
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;['夜峰美晴']
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class="language-python"&gt;print(res.line_joined.to_list())
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;['わたし達の声を通して、素敵な風を吹かせられたらいいなって･･･[br]それが、わたし達の進む道･･・・･･、 なんじゃないかな?']
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;割といい感じに取れていますね！&lt;/p&gt;
&lt;p&gt;実際はこの後、セリフ部分のテキストの正規化が必要になります。というのも、以下のような誤認識が頻発するためです。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;句読点の後にスペースが入る&lt;/li&gt;
&lt;li&gt;句読点がカンマやピリオドとして認識される&lt;/li&gt;
&lt;li&gt;…（三点リーダ）が誤認識される
&lt;ul&gt;
&lt;li&gt;・（中黒）として認識される&lt;/li&gt;
&lt;li&gt;その他、中黒に似た記号として認識される
&lt;ul&gt;
&lt;li&gt;三点リーダ1個が中黒か中黒に似た記号3個として認識されたりします。&lt;/li&gt;
&lt;li&gt;2個や4個として認識されたりもします。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;単語の一部文字だけ全く違う文字として認識される&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;特に記号は難易度が高いですね…。上の画像においても、読点の後に半角スペースが入っていたり、1個の三点リーダが中黒や半角中黒（UnicodeでU+FF65）3個として認識されていたり、また三点リーダが1個減っていたりします。NFKC正規化などで一旦普通の中黒に置き換えてから三点リーダに置換する必要があります。&lt;/p&gt;
&lt;h2 id="おわりに"&gt;おわりに&lt;/h2&gt;
&lt;p&gt;OCR自体はAPIに投げるだけなので楽ですね。時間がかかったのはStep2の漏れなくダブりなく画像を取り出すロジックの考案とStep4の前処理のロジック、Step5のOCRの後のテキストの正規化でした。&lt;/p&gt;</description></item><item><title>VPS（Ubuntu/Debian）でRとPythonとJuliaの開発環境を作る: part3</title><link>https://suzunano.net/posts/vps-setup-3/</link><pubDate>Mon, 26 Sep 2022 00:00:00 +0900</pubDate><guid>https://suzunano.net/posts/vps-setup-3/</guid><description>&lt;h2 id="はじめに"&gt;はじめに&lt;/h2&gt;
&lt;h3 id="概要"&gt;概要&lt;/h3&gt;
&lt;p&gt;VPS (Ubuntu/Debian) を借りて環境設定する際の、part1より進んだ設定の内容です。この記事の内容は必須ではありませんが、設定しておくとよりセキュリティレベルが上がったり、便利に使ったりすることができます。&lt;/p&gt;
&lt;p&gt;取り上げる内容はこちらです。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;fail2banの導入
&lt;ul&gt;
&lt;li&gt;繰り返しssh接続を試してくるIPアドレスをBAN&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;ssmtpの導入
&lt;ul&gt;
&lt;li&gt;監視用の簡易なメール送信クライアント&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;logwatchの導入&lt;/li&gt;
&lt;li&gt;落穂拾い
&lt;ul&gt;
&lt;li&gt;時刻のタイムゾーンをAsia/Tokyoにする&lt;/li&gt;
&lt;li&gt;デフォルトのシェルを変更する&lt;/li&gt;
&lt;li&gt;sysstatの導入&lt;/li&gt;
&lt;li&gt;ベンチマークを取る（Unixbench、speedtest）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="環境"&gt;環境&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;VPS
&lt;ul&gt;
&lt;li&gt;ConoHa VPS（メモリ1GB）&lt;/li&gt;
&lt;li&gt;Linux
&lt;ul&gt;
&lt;li&gt;Ubuntu: Ubuntu 22.04.1 LTS / Debian: Debian 11&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;ローカル環境
&lt;ul&gt;
&lt;li&gt;Windows 10 Home&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="関連記事"&gt;関連記事&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="../vps-setup-1/"&gt;VPS（Ubuntu/Debian）でRとPythonとJuliaの開発環境を作る: part1 - suzuna's memo&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="../vps-setup-2/"&gt;VPS（Ubuntu/Debian）でRとPythonとJuliaの開発環境を作る: part2 - suzuna's memo&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="fail2ban"&gt;fail2ban&lt;/h2&gt;
&lt;p&gt;VPSを起動してしばらく放っておくと、ssh接続を試みてくるIPアドレスが出現します。&lt;/p&gt;
&lt;p&gt;fail2banを導入すると、一定時間以内に一定回数以上ssh接続に失敗したIPアドレスを、一定時間ssh接続できないように弾くことができます。part1の設定の通り、パスワードログインを廃止し、ssh鍵でしかログインできないようにしておけばあまり気にしなくてもいいのですが、fail2banで簡単にBANできるので設定します。&lt;/p&gt;
&lt;p&gt;ちなみにssh接続のログは&lt;code&gt;/var/log/auth.log&lt;/code&gt;ですので、ssh接続を試みてくるIPアドレスは、&lt;code&gt;sudo cat /var/log/auth.log&lt;/code&gt;で見ることができます。&lt;/p&gt;
&lt;p&gt;まず、fail2banをインストールします。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;sudo apt install fail2ban
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;fail2banは、sshdなどの各種サービスのログファイルを監視するものです。filter, action, jailから構成されます。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;filter
&lt;ul&gt;
&lt;li&gt;各種サービスのログファイルにどのような文字列が表れたら攻撃と判定するか
&lt;ul&gt;
&lt;li&gt;/etc/fail2ban/filter.d/*.conf&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;action
&lt;ul&gt;
&lt;li&gt;攻撃があった場合にどう動作するか
&lt;ul&gt;
&lt;li&gt;/etc/fail2ban/action.d/*.conf&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;jail
&lt;ul&gt;
&lt;li&gt;filterとactionの組み合わせや、actionが発動する閾値（攻撃回数・時間数）などを定める
&lt;ul&gt;
&lt;li&gt;/etc/fail2ban/jail.conf&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;sshdを含むメジャーなサービスのfilterやactionはデフォルトで用意されているので、ここではjailのルールを編集するだけで導入ができます。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;/etc/fail2ban/jail.conf&lt;/code&gt;はアップデートで書き換えられる恐れがあるそうなので、&lt;code&gt;/etc/fail2ban/jail.local&lt;/code&gt;を編集します。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;# /etc/fail2ban/jail.localが存在しない場合だけ、最初に作っておく
sudo touch /etc/fail2ban/jail.local
sudo nano /etc/fail2ban/jail.local
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;jail.localがnanoで開いたら、以下の4行を記載して保存します。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;[sshd]
enabled = true
bantime = 86400
findtime = 3600
maxretry = 10
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;findtime秒以内にmaxretry回sshの接続に失敗したIPアドレスは、bantime（秒）の間sshの接続をブロックするという意味です。bantime, findtime, maxretryは好みに応じて値を書き換えてください。&lt;/p&gt;
&lt;p&gt;保存したらfail2banを起動し、Ubuntu起動時に自動で起動するようにします。今記載したjail.localを反映させるため、既に起動されている場合は&lt;code&gt;sudo systemctl stop fai2ban&lt;/code&gt;を実行してから以下のコマンドを入力してください。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;sudo systemctl start fail2ban
sudo systemctl enable fail2ban
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;うまく起動できていて、かつ自動起動も有効になっていることを確認します。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;sudo systemctl status fail2ban
# 以下の通り表示されればOK
# /lib/systemd/system/fail2ban.service; enabled: Ubuntu起動時に起動する設定になっている
# Active: active: 現在起動している
Loaded: loaded (/lib/systemd/system/fail2ban.service; enabled; vendor preset: enabled)
Active: active (running)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;なお、この後再度jail.localを書き換えた場合は&lt;code&gt;sudo systemctl restart fail2ban&lt;/code&gt;でfail2banを再起動してください。&lt;/p&gt;
&lt;p&gt;今現在ブロックされているIPアドレスは、こちらで確認できます。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;sudo fail2ban-client status sshd
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;また、fail2banのログである&lt;code&gt;/var/log/fail2ban.log&lt;/code&gt;を見ると、filterやaction、banのログが確認できます。&lt;/p&gt;
&lt;p&gt;fail2banの基準は当然自分にも当てはまりますので、自分も接続に失敗するとssh接続ができなくなります。自分が引っかかった場合は、ConoHaのWebのコンソールからログインし、以下で解除することができます。（jail.localに自分のIPアドレスをignoreipとして設定すれば自分を除外できますが、固定IPでないとあまり意味がないと思います）&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;sudo fail2ban-client set sshd unbanip [解除したい自分のIPアドレス]
# 設定を反映する
sudo fail2ban-client restart
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id="ssmtp"&gt;sSMTP&lt;/h2&gt;
&lt;p&gt;sSMTPとはメール送信専用（受信はできない）クライアントです。自前でSMTPサーバを用意せずに外部のSMTPサーバを使ってメールを送信する仕組みであり、Postfixなどより導入が簡単です。&lt;/p&gt;
&lt;p&gt;Gmailなどの既に持っているメールアドレスを使ってVPSからメールの送信をしたいという場合に役に立ちます。特に後述のlogwatchでメールを送信する際に使えます。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;sudo apt install ssmtp
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;設定ファイルの/etc/ssmtp/ssmtp.confを編集します。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;sudo nano /etc/ssmtp/ssmtp.conf
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;以下はGmailを使って送信したい場合の例です。イコールの右辺は自分のメール環境に合わせてください。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;root=postmaster
mailhub=smtp.gmail.com:587
rewriteDomain=gmail.com
hostname=gmail.com
FromLineOverride=YES
UseSTARTTLS=YES
AuthUser=[YOUR_GMAIL_ACCOUNT_NAME]@gmail.com
AuthPass=[YOUR_GMAIL_LOGIN_PASSWORD]
AuthMethod=LOGIN
TLS_CA_File=/etc/pki/tls/certs/ca-bundle.crt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;cronのエラー出力などのroot宛に送られるメールは、1行目のrootの行に書いた宛先に送られます。デフォルトはpostmasterです。&lt;/p&gt;
&lt;p&gt;保存したら送信テストをしてみます。以下の通り、インタラクティブにメールを書くことができます。Toで書いたアドレス宛にメールが届けば成功です。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;sendmail -t #ここでEnter
From: [受信時に見せたいメールアドレス] #ここでEnter
To: [宛先のメールアドレス] #ここでEnter
Subject: [題名] #ここでEnter
[以下本文] #ここでEnter。書き終わったらCtrl+Dで送信
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id="logwatch"&gt;logwatch&lt;/h2&gt;
&lt;p&gt;ログを取って1日に1回メールで送ってくれます。&lt;/p&gt;
&lt;p&gt;先に&lt;code&gt;sudo apt install ssmtp&lt;/code&gt;でsSMTPをインストールしておいてください。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;sudo apt install logwatch
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;デフォルトの設定は&lt;code&gt;/usr/share/logwatch/default.conf/logwatch.conf&lt;/code&gt;です。これを&lt;code&gt;/etc/logwatch/conf/logwatch.conf&lt;/code&gt;にコピーし、&lt;code&gt;/etc/logwatch/conf/logwatch.conf&lt;/code&gt;を編集することにします。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;sudo cp /usr/share/logwatch/default.conf/logwatch.conf /etc/logwatch/conf/logwatch.conf
sudo nano /etc/logwatch/conf/logwatch.conf
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;MailTo: root&lt;/code&gt;と書かれた行があるので、&lt;code&gt;root&lt;/code&gt;を受信したいメールアドレスにします。なお、&lt;code&gt;root&lt;/code&gt;のままにしている場合、sSMTPを導入済で、かつ&lt;code&gt;/etc/ssmtp/ssmtp.conf&lt;/code&gt;の&lt;code&gt;root&lt;/code&gt;に何らかのメールアドレスを書いていると、そのメールアドレス宛にメールが送られます（logwatchがrootにメールを送り、sSMTPがroot宛のメールを転送するから）。&lt;/p&gt;
&lt;p&gt;保存したら、logwatchのテストを行います。以下の2行を順番に実行します。1行目ではコンソールにログが表示され、2行目ではメールが届けば成功です。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;logwatch --output stdout
logwatch --output mail
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;私はこんなエラーが出たのですが、ググったところ&lt;code&gt;/var/cache/logwatch&lt;/code&gt;が存在しないことが原因のようで、&lt;code&gt;sudo mkdir /var/cache/logwatch&lt;/code&gt;すると正しく動作しました。（参考: &lt;a href="https://keimlab.hatenablog.com/entry/2020/01/02/134151" target="_blank" rel="noopener noreferrer"&gt;Logwatch設定 - keimlab’s diary&lt;/a&gt;）&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;/var/cache/logwatch No such file or directory at /usr/sbin/logwatch line 651.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;なお、毎日1回logwatchからメールが届くようになります。というのも、&lt;code&gt;/etc/cron.daily/00logwatch&lt;/code&gt;に以下の通り記述されているからです。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;#!/bin/bash
#Check if removed-but-not-purged
test -x /usr/share/logwatch/scripts/logwatch.pl || exit 0
#execute
/usr/sbin/logwatch --output mail #これ
#Note: It's possible to force the recipient in above command
#Just pass --mailto address@a.com instead of --output mail
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;logwatchのメールを受け取りたくない場合は、上の&lt;code&gt;/usr/sbin/logwatch --output mail&lt;/code&gt;をコメントアウトするとメールが送られなくなります。&lt;/p&gt;
&lt;h2 id="落穂拾い"&gt;落穂拾い&lt;/h2&gt;
&lt;h3 id="タイムゾーンをjstに変更"&gt;タイムゾーンをJSTに変更&lt;/h3&gt;
&lt;p&gt;タイムゾーンはデフォルトではUTCになっていることがあります。以下の通りJSTに変更することができます。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;date&lt;/code&gt;コマンドか&lt;code&gt;timedatectl&lt;/code&gt;コマンドで今の時刻を確認します。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;JST&lt;/code&gt;、あるいは&lt;code&gt;Asia/Tokyo&lt;/code&gt;と書いてあればタイムゾーンはJSTになっています。&lt;code&gt;UTC&lt;/code&gt;などと、JST以外の設定になっていたら下記でJSTに変更します。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;sudo timedatectl set-timezone Asia/Tokyo
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;再度&lt;code&gt;date&lt;/code&gt;コマンドか&lt;code&gt;timedatectl&lt;/code&gt;コマンドを実行し、&lt;code&gt;JST&lt;/code&gt;か&lt;code&gt;Asia/Tokyo&lt;/code&gt;とあれば成功です。&lt;/p&gt;
&lt;h3 id="デフォルトのシェルを変更"&gt;デフォルトのシェルを変更&lt;/h3&gt;
&lt;p&gt;デフォルトではshが使われていますが、bashに変えたいという場合は以下の通りコマンドを打ちます。&lt;/p&gt;
&lt;p&gt;まずbashのパスを調べます。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;which bash
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;例えば&lt;code&gt;/usr/bin/bash&lt;/code&gt;と表示されたら、&lt;code&gt;chsh&lt;/code&gt;で以下の通り変更します。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;chsh -s /usr/bin/bash
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;再ログインするとbashに変わっているはずです。&lt;/p&gt;
&lt;h3 id="sysstat"&gt;sysstat&lt;/h3&gt;
&lt;p&gt;CPU使用率やメモリ使用量などの情報をリアルタイムで表示したり、過去の値を後から確認するのに使えます。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;sudo apt install sysstat
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;インストールできたら、リアルタイムの情報を確認してみます。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;# CPU使用率を1秒間隔で表示する
sar 1
# メモリ使用率を3秒間隔で表示する
sar -r 3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;インストール後はCPU使用率などのログが自動的に&lt;code&gt;/var/log/sysstat/[sa + 日付]&lt;/code&gt;に保存されています。インストールしてしばらく経ってから&lt;code&gt;sar&lt;/code&gt;や&lt;code&gt;sar -r&lt;/code&gt;とコマンドを打ってみると、過去のシステム使用状況の情報が見られます。&lt;/p&gt;
&lt;h3 id="ベンチマークを取る"&gt;ベンチマークを取る&lt;/h3&gt;
&lt;h4 id="unixbench"&gt;Unixbench&lt;/h4&gt;
&lt;p&gt;システムのパフォーマンスを測定するベンチマークです。ソースは&lt;a href="https://github.com/kdlucas/byte-unixbench" target="_blank" rel="noopener noreferrer"&gt;kdlucas/byte-unixbench&lt;/a&gt;にありますので、ここからgit cloneしてビルドします。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;cd [byte-unixbenchを展開したい適当なディレクトリ]
git clone https://github.com/kdlucas/byte-unixbench
# ビルドに使う
sudo apt install build-essential
cd byte-unixbench/UnixBench
./Run
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;数十分ほど待つとベンチマークの結果が表示されます。&lt;/p&gt;
&lt;h4 id="speedtest"&gt;Speedtest&lt;/h4&gt;
&lt;p&gt;回線速度を測定するものです。&lt;a href="https://www.speedtest.net/" target="_blank" rel="noopener noreferrer"&gt;Speedtest by Ookla&lt;/a&gt;が提供しているCLIツールを使います。&lt;/p&gt;
&lt;p&gt;&lt;a href="https://www.speedtest.net/ja/apps/cli" target="_blank" rel="noopener noreferrer"&gt;こちらのインストール方法&lt;/a&gt;の通りインストールして実行します。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;sudo apt install curl
curl -s https://packagecloud.io/install/repositories/ookla/speedtest-cli/script.deb.sh | sudo bash
sudo apt install speedtest
speedtest
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id="参考にしたサイト"&gt;参考にしたサイト&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;fail2ban
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://gihyo.jp/lifestyle/serial/01/ganshiki-soushi/0094" target="_blank" rel="noopener noreferrer"&gt;第94回　サイトの防御とFail2ban［その1］ | gihyo.jp&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://knowledge.sakura.ad.jp/7377/#fail2ban" target="_blank" rel="noopener noreferrer"&gt;不正アクセスからサーバを守るfail2ban。さくらのクラウド、VPSで使ってみよう！ | さくらのナレッジ&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;sSMTP
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://to-31.blogspot.com/2019/09/centos7-ssmtp.html" target="_blank" rel="noopener noreferrer"&gt;CentOS7 上で SSMTPの設定&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://bashalog.c-brains.jp/12/07/31-185952.php" target="_blank" rel="noopener noreferrer"&gt;[linux][sSMTP] 自前で smtp サーバを用意せずにメール送信する | バシャログ。&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;その他
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://qiita.com/sabaku20XX/items/97db2c0bf7298e3a645c" target="_blank" rel="noopener noreferrer"&gt;UbuntuのOSやCPU, GPUの情報を確認するコマンド - Qiita&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>VPS（Ubuntu/Debian）でRとPythonとJuliaの開発環境を作る: part2</title><link>https://suzunano.net/posts/vps-setup-2/</link><pubDate>Thu, 22 Sep 2022 00:00:00 +0900</pubDate><guid>https://suzunano.net/posts/vps-setup-2/</guid><description>&lt;h2 id="はじめに"&gt;はじめに&lt;/h2&gt;
&lt;h3 id="概要"&gt;概要&lt;/h3&gt;
&lt;p&gt;VPS (Ubuntu/Debian) でのRとPythonとJuliaの開発環境の作り方です。このpart2では、R, Python (Miniconda), Julia, Gitの設定方法を書きます。&lt;/p&gt;
&lt;h3 id="環境"&gt;環境&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;VPS
&lt;ul&gt;
&lt;li&gt;ConoHa VPS（メモリ1GB）&lt;/li&gt;
&lt;li&gt;Linux
&lt;ul&gt;
&lt;li&gt;Ubuntu: Ubuntu 22.04.1 LTS / Debian: Debian 11&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;R 4.2.1&lt;/li&gt;
&lt;li&gt;Miniconda 4.12.0 + Python 3.10.4&lt;/li&gt;
&lt;li&gt;Julia 1.7.3&lt;/li&gt;
&lt;li&gt;Git 2.34.1&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;ローカル環境
&lt;ul&gt;
&lt;li&gt;Windows 10 Home&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="関連記事"&gt;関連記事&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="../vps-setup-1/"&gt;VPS（Ubuntu/Debian）でRとPythonとJuliaの開発環境を作る: part1 - suzuna's memo&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="../vps-setup-3/"&gt;VPS（Ubuntu/Debian）でRとPythonとJuliaの開発環境を作る: part3 - suzuna's memo&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="rのインストール"&gt;Rのインストール&lt;/h2&gt;
&lt;p&gt;この章は、UbuntuとDebianで入力するコマンドが異なります。&lt;/p&gt;
&lt;p&gt;&lt;a href="https://cran.r-project.org/" target="_blank" rel="noopener noreferrer"&gt;CRAN&lt;/a&gt;のトップページに記載の&amp;quot;Download R for Linux&amp;quot;のリンクの通り、以下を実行してインストールします。&lt;a href="https://cran.r-project.org/bin/linux/ubuntu/" target="_blank" rel="noopener noreferrer"&gt;Ubuntu&lt;/a&gt;と&lt;a href="https://cran.r-project.org/bin/linux/debian/" target="_blank" rel="noopener noreferrer"&gt;Debian&lt;/a&gt;でリンクが異なりますので、使っているLinuxのディストリビューションに合わせてリンクを参照します。&lt;/p&gt;
&lt;h3 id="ubuntu"&gt;Ubuntu&lt;/h3&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;sudo apt update -qq
sudo apt install --no-install-recommends software-properties-common dirmngr
wget -qO- https://cloud.r-project.org/bin/linux/ubuntu/marutter_pubkey.asc | sudo tee -a /etc/apt/trusted.gpg.d/cran_ubuntu_key.asc
sudo add-apt-repository &amp;quot;deb https://cloud.r-project.org/bin/linux/ubuntu $(lsb_release -cs)-cran40/&amp;quot;
sudo apt install --no-install-recommends r-base
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id="debian"&gt;Debian&lt;/h3&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;sudo apt install software-properties-common
sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-key &amp;quot;95C0FAF38DB3CCAD0C080A7BDC78B2DDEABC47B7&amp;quot;
sudo add-apt-repository &amp;quot;deb http://cloud.r-project.org/bin/linux/debian bullseye-cran40/&amp;quot;
sudo apt update
sudo apt install r-base r-base-dev
# どちらか片方（線形代数の計算を高速にするライブラリ）
sudo apt install libatlas3-base
sudo apt install libopenblas-base
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;UbuntuとDebianのどちらでも、終わったら、&lt;code&gt;R&lt;/code&gt;と入力してRの対話環境が出てくればインストールできています。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;install.packages()&lt;/code&gt;でライブラリをインストールする際、パッケージがないためにインストールに失敗することがあります。私の環境では少なくとも&lt;code&gt;install.packages(&amp;quot;tidyverse&amp;quot;)&lt;/code&gt;でこのエラーを確認しました。同じエラーは&lt;a href="https://qiita.com/hachisukansw/items/ac1b7f608db1fe4d09e6" target="_blank" rel="noopener noreferrer"&gt;Rでパッケージがインストールできない時の対処法メモ&lt;/a&gt;でも確認しました。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;install.packages()&lt;/code&gt;する前に、以下のコマンドでパッケージを入れておくといいと思います。以下のaptのパッケージは、tidyverseのインストールで必要なもの以外に、他のパッケージのインストールで必要なものも含みます（何のパッケージで必要だったか忘れました…）。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;sudo apt install build-essential libcurl4-openssl-dev xorg-dev libssl-dev libxml2-dev
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id="python-miniconda-のインストール"&gt;Python (Miniconda) のインストール&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://docs.conda.io/en/latest/miniconda.html" target="_blank" rel="noopener noreferrer"&gt;Miniconda公式のダウンロードページ&lt;/a&gt;より、インストールしたいバージョンのMinicondaを選び、ダウンロードリンクのURLをローカルにコピーしておきます。ここでは最新版の&amp;quot;https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh&amp;quot;をインストールすることにします。&lt;/p&gt;
&lt;p&gt;なお、Linuxのインストーラーはx86やARMなどプラットフォームごとに分かれています。使っているVPSのプラットフォームは&lt;code&gt;uname -m&lt;/code&gt;で調べられます。以下、x86の前提で進めていきます。&lt;/p&gt;
&lt;p&gt;適当なディレクトリにインストーラーのシェルファイルをwgetでダウンロードして、bashでインストーラーを実行します。ここではユーザーディレクトリ直下にdownloadsディレクトリを作り、そこにダウンロードすることにします。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;cd ~
mkdir downloads
cd downloads
wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh
# &amp;quot;Miniconda3-latest-Linux-x86_64.sh&amp;quot;は上のwgetのファイル名と合わせる
bash Miniconda3-latest-Linux-x86_64.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;次に&lt;code&gt;~/.bashrc&lt;/code&gt;をnanoなどの適当なエディタで開き、最終行以降に以下の2行を書いて上書き保存します。私の環境では1行目は既に&lt;code&gt;~/.bashrc&lt;/code&gt;内に書いてあったので、2行目だけを記載しました。&lt;code&gt;~/.bashrc&lt;/code&gt;がなければ&lt;code&gt;touch ~/.bashrc&lt;/code&gt;で作成してください。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;export PATH=~/miniconda3/bin:$PATH
source ~/miniconda3/etc/profile.d/conda.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;1行目はminicondaのパスを通しています。2行目は&lt;code&gt;conda activate&lt;/code&gt;のエラー対策です。例えばこの辺をご覧ください。（&lt;a href="https://qiita.com/peaceiris/items/e41488096e765269c93c" target="_blank" rel="noopener noreferrer"&gt;conda activate の CommandNotFoundError への対処方法 - Qiita&lt;/a&gt;）&lt;/p&gt;
&lt;p&gt;上書き保存したら以下を実行して、&lt;code&gt;~/.bashrc&lt;/code&gt;の内容を読み込みます。これを実行しないと設定内容が反映されません。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;source ~/.bashrc
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;ターミナルから&lt;code&gt;python&lt;/code&gt;と打ってPythonの対話環境が出れば成功です。&lt;/p&gt;
&lt;h2 id="juliaのインストール"&gt;Juliaのインストール&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://julialang.org/downloads/" target="_blank" rel="noopener noreferrer"&gt;Download Julia&lt;/a&gt;より、インストールしたいバージョンのインストーラーのURLをローカルにコピーしておきます。&lt;/p&gt;
&lt;p&gt;適当なフォルダにインストーラーをダウンロードしてから解凍します。ここではユーザーディレクトリ直下にjulia173というディレクトリを作ってそこに解凍しています。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;cd ~/downloads
wget https://julialang-s3.julialang.org/bin/linux/x64/1.7/julia-1.7.3-linux-x86_64.tar.gz
mkdir ~/julia173
tar zxvf ~/downloads/julia-1.7.3-linux-x86_64.tar.gz -C ~/julia173
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;終わったら、Juliaにパスを通すため、最後に&lt;code&gt;~/.bashrc&lt;/code&gt;の最終行に次の1行を追記して上書き保存します。&lt;code&gt;julia173/julia-1.7.3&lt;/code&gt;の部分は自分の環境のJuliaのディレクトリに変えてください。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;export PATH=&amp;quot;$PATH:~/julia173/julia-1.7.3/bin&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上書き保存したらMinicondaの場合と同じく&lt;code&gt;source ~/.bashrc&lt;/code&gt;を実行してください。その後&lt;code&gt;julia&lt;/code&gt;と入力してJuliaの対話環境が出れば成功です。&lt;/p&gt;
&lt;h2 id="gitのインストールgithubをsshで使えるようにする"&gt;Gitのインストール＋GitHubをsshで使えるようにする&lt;/h2&gt;
&lt;p&gt;まずaptでGitをインストールします。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;sudo apt install git
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;次に秘密鍵・公開鍵を作り、GitHubに登録します。&lt;/p&gt;
&lt;p&gt;まず鍵の作成です。VPSから以下を実行します。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;cd ~/.ssh
# 700のパーミッションを設定していない場合は以下で設定する
# chmod 700 ~/.ssh
# -f以降は好きなファイル名にする。これを入力した後、いくつか質問されるが全部Enterを押して大丈夫
ssh-keygen -t ed25519 -f github_conoha
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;~/.ssh&lt;/code&gt;に、github_conoha（秘密鍵）、github_conoha.pub（公開鍵）の2つのファイルができています。このうち公開鍵のgithub_conoha.pubをnanoか何か適当なエディタで開き、中身を全選択してクリップボードにコピーします。&lt;/p&gt;
&lt;p&gt;そうしたら次に公開鍵をGitHubに登録します。ローカルPCからGitHubを開き、settings &amp;gt; sshから先程の公開鍵を貼り付けます。&lt;/p&gt;
&lt;p&gt;再度VPSに戻り、&lt;code&gt;~/.ssh/config&lt;/code&gt;に以下を記載して保存すればOKです。（&lt;code&gt;~/.ssh/config&lt;/code&gt;が存在しない場合は、&lt;code&gt;touch ~/.ssh/config&lt;/code&gt;で作ってください）3行目は先程作った秘密鍵のパスを指定します。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;Host github.com
HostName github.com
IdentityFile ~/.ssh/github_conoha
User git
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;保存したら、以下を実行します。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;ssh -T git@github.com
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Hi [Your Username]! You've successfully authenticated, but GitHub does not provide shell access.&lt;/code&gt;と表示されればGitHubとssh接続ができています。&lt;/p&gt;
&lt;p&gt;ここまで終われば、R, Python, Julia, Gitを使える開発環境が整いました。お疲れ様でした。&lt;/p&gt;
&lt;h2 id="参考"&gt;参考&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;公式ドキュメント
&lt;ul&gt;
&lt;li&gt;R
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://cran.r-project.org/bin/linux/ubuntu/" target="_blank" rel="noopener noreferrer"&gt;Ubuntu&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://cran.r-project.org/bin/linux/debian/" target="_blank" rel="noopener noreferrer"&gt;Debian&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Miniconda
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://conda.io/projects/conda/en/latest/user-guide/install/linux.html" target="_blank" rel="noopener noreferrer"&gt;Installing on Linux&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Julia
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://julialang.org/downloads/platform/" target="_blank" rel="noopener noreferrer"&gt;Platform Specific Instructions for Official Binaries&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;GitHub
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.github.com/ja/authentication/connecting-to-github-with-ssh" target="_blank" rel="noopener noreferrer"&gt;SSH を使用した GitHub への接続 - GitHub Docs&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>VPS（Ubuntu/Debian）でRとPythonとJuliaの開発環境を作る: part1</title><link>https://suzunano.net/posts/vps-setup-1/</link><pubDate>Mon, 05 Sep 2022 00:00:00 +0900</pubDate><guid>https://suzunano.net/posts/vps-setup-1/</guid><description>&lt;h2 id="はじめに"&gt;はじめに&lt;/h2&gt;
&lt;h3 id="概要"&gt;概要&lt;/h3&gt;
&lt;p&gt;VPS (Ubuntu/Debian) を借りてRとPythonとJuliaの開発環境を作るところまでの基本的な設定の仕方です。UbuntuでもDebianでもどちらでも同じように設定できます。ConoHa VPSを利用していますが、Ubuntu/DebianであればConoHaではない他のVPSでも同じように進められると思います。&lt;/p&gt;
&lt;p&gt;part1ではssh接続の設定について書きます。これらの設定を入れることにします。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;rootユーザでのsshログインを許可しない&lt;/li&gt;
&lt;li&gt;秘密鍵でのsshログインのみ許可し、パスワードログインを禁止&lt;/li&gt;
&lt;li&gt;ポート番号を22番から変更&lt;/li&gt;
&lt;li&gt;許可したポート番号以外のポート番号を閉じる&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="環境"&gt;環境&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;VPS
&lt;ul&gt;
&lt;li&gt;ConoHa VPS（メモリ1GB）&lt;/li&gt;
&lt;li&gt;Linux
&lt;ul&gt;
&lt;li&gt;Ubuntu: Ubuntu 22.04.1 LTS / Debian: Debian 11&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;ローカル環境
&lt;ul&gt;
&lt;li&gt;Windows 10 Home&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="関連記事"&gt;関連記事&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="../vps-setup-2/"&gt;VPS（Ubuntu/Debian）でRとPythonとJuliaの開発環境を作る: part2 - suzuna's memo&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="../vps-setup-3/"&gt;VPS（Ubuntu/Debian）でRとPythonとJuliaの開発環境を作る: part3 - suzuna's memo&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="vps契約"&gt;VPS契約&lt;/h2&gt;
&lt;p&gt;ブラウザからConoHaにログインして新しくVPSを立てます。詳しくは&lt;a href="https://support.conoha.jp/v/addvps/" target="_blank" rel="noopener noreferrer"&gt;公式のガイド&lt;/a&gt;を参照ください。&lt;/p&gt;
&lt;p&gt;「接続許可ポート」のオプションは、ポート開放はあとでVPS上からufwで行うので、「全て許可」にチェックを入れます。また、「SSH Key」は、後でローカルで作成するので、「使用しない」にチェックを入れます。&lt;/p&gt;
&lt;p&gt;なおConoHaではスペックのグレードを選ぶことができますが、メモリ1GB以上のプランを選んでおくといいです。VPSを立てた後からプランをアップグレード・ダウングレードできますが、512MBプランはその対象外であるためです。&lt;/p&gt;
&lt;h2 id="パッケージのアップデート"&gt;パッケージのアップデート&lt;/h2&gt;
&lt;p&gt;ローカルのWindowsのコマンドプロンプトを立ち上げ、rootユーザでsshログインします。パスワードを聞かれるので、VPSを立てる時にConoHaの設定画面で入力したrootパスワードを入力してください。&lt;/p&gt;
&lt;p&gt;なお、以下、かっこ（[]）で囲った部分は、何らかの値を入れることを示します。実際にコマンドを打つ際はかっこは不要です。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;# sshはデフォルトでは22番ポートでログインするので、&amp;quot;-p 22&amp;quot;は付けなくていい
ssh root@[VPSのIPアドレス]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;ログイン後、まずはパッケージをアップデートします。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;apt update -y
apt upgrade -y
apt dist-upgrade -y
apt --purge autoremove
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;再起動するようメッセージで求められた場合は&lt;code&gt;reboot&lt;/code&gt;で再起動してください。再起動されるまで数十秒程度かかりますので、そのくらいの時間待ってから再度rootユーザでsshログインします。&lt;/p&gt;
&lt;h2 id="一般ユーザの作成sudoユーザー化"&gt;一般ユーザの作成＋sudoユーザー化&lt;/h2&gt;
&lt;p&gt;セキュリティの観点からrootユーザではssh接続できないようにします。そのため、sudo権限を持つ一般ユーザーを作成し、このユーザーにsshでログインすることにします。&lt;/p&gt;
&lt;p&gt;ここから先、userというユーザー名のユーザーを作成することにします。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;# &amp;quot;-m&amp;quot;を付けることで、/home/直下にユーザーフォルダを作成する
useradd -m user
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;次にsshのログインパスワードを設定します。パスワードを聞かれるので、設定したいパスワードを入力してください。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;passwd user
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;ここまで終わったら、以下&lt;code&gt;id user&lt;/code&gt;と入力すると、&lt;code&gt;(sudo)&lt;/code&gt;の文字列は出てこないはずです。これは、userがsudo権限を持っていないユーザーであることを示します。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;id user
# (sudo)の文字列が出てこない = sudoerではない
# uid=xxxx(user) gid=xxxx(user) groups=xxxx(user)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;userをsudoerにします。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;gpasswd -a user sudo
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;ここまで終わったら、&lt;code&gt;id user&lt;/code&gt;と入力すると、&lt;code&gt;(sudo)&lt;/code&gt;の文字列が表示されるはずです。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;id user
# (sudo)の文字列が表示される = sudoer
# uid=xxxx(user) gid=xxxx(user) groups=xxxx(user) xxxx(sudo)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id="rootでのsshログイン禁止"&gt;rootでのsshログイン禁止&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;exit&lt;/code&gt;と打ってログインを切ってから、以下をローカルのコマンドプロンプトで打ってuserでログインします（ログインを切らずに&lt;code&gt;su user&lt;/code&gt;でもいいです）。パスワードを入力する必要があるので、&lt;code&gt;passwd user&lt;/code&gt;で設定したパスワードを入力してください。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;ssh user@[VPSのIPアドレス]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;ssh接続の設定ファイルである&lt;code&gt;/etc/ssh/sshd_config&lt;/code&gt;を編集します。sudo権限でないと編集できないので、sudoをコマンドの先頭に付けます。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;sudo nano /etc/ssh/sshd_config
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;/etc/ssh/sshd_config&lt;/code&gt;がnanoで開きます。&lt;code&gt;PermitRootLogin yes&lt;/code&gt;と書いてある行があるので、この&lt;code&gt;yes&lt;/code&gt;を&lt;code&gt;no&lt;/code&gt;に書き換え、保存して&lt;code&gt;sshd_config&lt;/code&gt;を閉じます。&lt;/p&gt;
&lt;p&gt;なお、nanoではCtrl + Oを押してからEnterを押して上書き保存し、次にCtrl + Xで閉じることができます。&lt;/p&gt;
&lt;p&gt;上書き保存してnanoを終了できたら、以下のコマンドで&lt;code&gt;sshd_config&lt;/code&gt;の変更内容を反映させます。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;sudo systemctl restart sshd
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;rootではsshログインできず、userではログインできることを確認しておく必要があります。&lt;/p&gt;
&lt;p&gt;ローカルでコマンドプロンプトを別窓で立ち上げ、&lt;code&gt;ssh root@[VPSのIPアドレス]&lt;/code&gt;ではログインできなくなったことを確認しておきます。次に&lt;code&gt;ssh user@[VPSのIPアドレス]&lt;/code&gt;ではログインできることも確認しておきます。&lt;/p&gt;
&lt;p&gt;このとき、今設定のために接続しているコマンドプロンプトは閉じず、別窓でコマンドプロンプトを開くことに注意してください。sshの設定に失敗して接続できなくなった場合、接続できていたコマンドプロンプトを閉じてしまうとsshでログインできなくなってしまいます。（仮にそうなったとしても、ConoHaではブラウザの設定画面からコンソールログインができるはずですが…。）以降もssh接続の設定を変える度に都度接続できることを確認していきますが、必ず今繋いでいるコマンドプロンプトを閉じないで別窓で確認するようにします。&lt;/p&gt;
&lt;h2 id="ssh鍵の作成と登録"&gt;ssh鍵の作成と登録&lt;/h2&gt;
&lt;p&gt;ローカルに、VPSと接続するための秘密鍵と公開鍵を作ります。&lt;/p&gt;
&lt;p&gt;rootユーザでログインしているコマンドプロンプトを閉じてssh接続を切ってから、以下を入力します。ユーザーフォルダ直下に.sshフォルダが存在しなければ先に.sshフォルダを作成してください。-fの後には鍵のファイルに付けたいファイル名を入れます。ここでは仮でhogeとしておきます。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;cd [ユーザーフォルダ直下の.sshフォルダのパス]
ssh-keygen -t ed25519 -f hoge
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;passphraseを入力してくださいなどと3回ほど入力を求められるはずですが、全て何も入力せずにEnterキーを押してください。&lt;/p&gt;
&lt;p&gt;そうすると、ユーザーフォルダの直下の.sshフォルダにhogeとhoge.pubという二つのファイルが作られていると思います。前者は秘密鍵、後者は公開鍵です。&lt;/p&gt;
&lt;p&gt;次に、今作成した鍵のうち、公開鍵（hoge.pub）をVPSの&lt;code&gt;~/.ssh/authorized_keys&lt;/code&gt;に保存します。&lt;code&gt;ssh user@[VPSのIPアドレス]&lt;/code&gt;でVPSのuserにsshログインしてから、ターミナルで以下を入力します。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;cd ~
mkdir .ssh
chmod 700 .ssh
touch .ssh/authorized_keys
nano .ssh/authorized_keys
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;nanoで&lt;code&gt;~/.ssh/authorized_keys&lt;/code&gt;が開きます。ここでローカルで&lt;code&gt;hoge.pub&lt;/code&gt;を開き、中身を全選択 -&amp;gt; クリップボードにコピーし、sshで接続しているコマンドプロンプトのnanoの画面に貼り付けて上書き保存してください。&lt;/p&gt;
&lt;p&gt;念のため&lt;code&gt;cat .ssh/authorized_keys&lt;/code&gt;を実行し、ローカルの&lt;code&gt;hoge.pub&lt;/code&gt;と同じものが貼り付けられていることを確認します。&lt;/p&gt;
&lt;p&gt;最後に&lt;code&gt;.ssh/authorized_keys&lt;/code&gt;に600のパーミッションを設定します。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;chmod 600 .ssh/authorized_keys
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;ここで、正常に秘密鍵でssh接続できることを確認します。今VPSにssh接続しているコマンドプロンプトとは別窓でコマンドプロンプトを立ち上げ、以下を入力してssh接続できることを確かめてください。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;ssh user@[VPSのIPアドレス] -i [ローカルの秘密鍵hogeのフルパス]
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id="パスワードログインの禁止"&gt;パスワードログインの禁止&lt;/h2&gt;
&lt;p&gt;次に、全てのユーザでパスワードログインできないようにします。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;sudo nano /etc/ssh/sshd_config
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;PasswordAuthentication yes&lt;/code&gt;と書いてある行があるので、その&lt;code&gt;yes&lt;/code&gt;を&lt;code&gt;no&lt;/code&gt;に変えて上書き保存します。私の場合、&lt;code&gt;PasswordAuthentication&lt;/code&gt;は2箇所あったので、2箇所とも&lt;code&gt;no&lt;/code&gt;に変えました。&lt;/p&gt;
&lt;p&gt;以下で設定を反映させます。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;sudo systemctl restart sshd
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;今ssh接続しているコマンドプロンプトとは別窓でコマンドプロンプトを立ち上げ、パスワード認証ではログインできず、かつ鍵ではログインできることを確認します。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;# パスワードログインしてみる
# これはエラーでログインできないことを確認する
ssh user@[VPSのIPアドレス] -o PreferredAuthentications=password
# 次に、鍵でログインしてみる
# これはログインできることを確認する
ssh user@[VPSのIPアドレス] -i [ローカルの秘密鍵hogeのフルパス]
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id="ポート開放sshのポート番号の変更"&gt;ポート開放＋sshのポート番号の変更&lt;/h2&gt;
&lt;p&gt;sshのポート番号はデフォルトでは22番ですが、これを他の適当な番号に変えます。&lt;/p&gt;
&lt;p&gt;ポート番号は0番～65535番まであり、うち0番～1023番のWell-known Portは他のサービスで使われることもあるので、1024番以上のポートにしましょう。1024番～49151番のRegistered Portも使われていることがあるので、好みが特になければユーザーが自由に使えることになっている49152番以降のポート番号にするといいと思います。ここでは仮に50022番に変更することにします。&lt;/p&gt;
&lt;p&gt;まず、念のため50022番がポート番号として使われていないことを確認します。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;sudo lsof -i:50022
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;何もコンソールに表示されなければ使われていないので問題ありません。&lt;/p&gt;
&lt;h3 id="ポート開放"&gt;ポート開放&lt;/h3&gt;
&lt;p&gt;ポート開放の設定に使うufwをインストールし、OSが起動した際に自動起動するようにします。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;sudo apt install ufw
sudo systemctl enable ufw
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;それでは次に、ufwを使ってファイアウォールの設定を変更し、50022番を通すようにします。その前に、50022番が開放されていないことを念のため確認します。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;sudo ufw status
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;50022番がallowされていなければ、開放する必要があります。allowされていれば以下の開放の必要はありません。（私が試した所、初期状態では22番のOpenSSHしか開放されていませんでした）&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;sudo ufw allow 50022
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;再度&lt;code&gt;sudo ufw status&lt;/code&gt;と打って、50022番がallowとなっていればOKです。&lt;/p&gt;
&lt;h3 id="ssh接続のポート番号の変更"&gt;ssh接続のポート番号の変更&lt;/h3&gt;
&lt;p&gt;そしたら、&lt;code&gt;sshd_config&lt;/code&gt;を編集してssh接続に使用するポート番号を変更します。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;sudo nano /etc/ssh/sshd_config
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Port 22&lt;/code&gt;と書いてある行があると思います。この&lt;code&gt;22&lt;/code&gt;を&lt;code&gt;50022&lt;/code&gt;に変更して上書き保存します。&lt;/p&gt;
&lt;p&gt;以下で設定を反映させます。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;sudo systemctl restart sshd
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最後に、今ssh接続しているコマンドプロンプトとは別窓でコマンドプロンプトを立ち上げ、22番ではログインできず、50022番ではログインできることを確認します。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;# これはエラーでログインできないことを確認する
ssh user@[VPSのIPアドレス] -i [ローカルの秘密鍵hogeのフルパス] -p 22
# 次に、これはログインできることを確認する
ssh user@[VPSのIPアドレス] -i [ローカルの秘密鍵hogeのフルパス] -p 50022
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;これでひとまず最低限のssh接続のセキュリティ確保の設定が終わりました。お疲れ様でした。&lt;/p&gt;
&lt;p&gt;ConoHaでは&lt;a href="https://support.conoha.jp/v/saveimages/" target="_blank" rel="noopener noreferrer"&gt;VPSのディスクイメージを保存しておく&lt;/a&gt;ことができます。VPSを削除しても、新しくVPSを作る際に、保存したディスクイメージから復元することができ便利です。この段階で一旦VPSのディスクイメージを作っておくと役立つかもしれません。&lt;/p&gt;
&lt;h2 id="ローカルのconfigにsshログインの設定を記載"&gt;ローカルのconfigにsshログインの設定を記載&lt;/h2&gt;
&lt;p&gt;さて、これからは&lt;code&gt;ssh user@[VPSのIPアドレス] -i [ローカルの秘密鍵hogeのフルパス] -p 50022&lt;/code&gt;でログインするわけですが、毎回これを入力するのは面倒です。それを簡略化できるように、ローカルの&lt;code&gt;~/.ssh/config&lt;/code&gt;にssh接続の設定を記載します。&lt;/p&gt;
&lt;p&gt;ローカルのユーザーフォルダ直下の&lt;code&gt;.ssh/config&lt;/code&gt;を適当なエディタで開き、以下を追記して上書き保存します。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;Host conoha
HostName [VPSのIPアドレス]
User user # VPSで接続したいユーザー名。ここでは先程作ったuser
Port 50022
IdentityFile [上で作成した秘密鍵 (hoge) のフルパス]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Host conoha&lt;/code&gt;の&lt;code&gt;conoha&lt;/code&gt;は好きな名前にしてください。ただし、&lt;code&gt;.ssh/config&lt;/code&gt;の中では一意な名前にする必要があります。&lt;/p&gt;
&lt;p&gt;Identity Fileは、Windowsではファイルパスの区切り文字はバックスラッシュですが、バックスラッシュでもUnix風のスラッシュ（&lt;code&gt;~/.ssh/hoge&lt;/code&gt;）でもどちらでも構いません。&lt;/p&gt;
&lt;p&gt;これにより、今後は&lt;code&gt;ssh user@[VPSのIPアドレス] -i [ローカルの秘密鍵hogeのフルパス] -p 50022&lt;/code&gt;の代わりに&lt;code&gt;ssh conoha&lt;/code&gt;でログインできるようになります。&lt;/p&gt;
&lt;h2 id="ローカルのvscodeに拡張機能remote-developmentをインストール"&gt;ローカルのVSCodeに拡張機能Remote Developmentをインストール&lt;/h2&gt;
&lt;p&gt;VSCodeの拡張機能Remote Developmentを入れると、VSCode上でssh接続し、あたかもローカルのファイルを操作しているかのように作業ができます。入れ方は良記事がたくさんあるのでググってみてください。&lt;/p&gt;
&lt;p&gt;この拡張機能は色々便利なのですが、VSCodeからssh接続してターミナルで&lt;code&gt;code [開きたいファイルのパス]&lt;/code&gt;と打つと、VSCode上でそのファイルが開かれて編集できることが中々快適です。&lt;code&gt;nano [開きたいファイルのパス]&lt;/code&gt;の代わりになります。&lt;/p&gt;</description></item><item><title>ニコニコ動画の検索WebアプリをShinyで作った</title><link>https://suzunano.net/posts/nicosearch/</link><pubDate>Fri, 29 Apr 2022 00:00:00 +0900</pubDate><guid>https://suzunano.net/posts/nicosearch/</guid><description>&lt;h2 id="概要"&gt;概要&lt;/h2&gt;
&lt;p&gt;ニコニコ動画の検索アプリを作りました。→&lt;a href="https://suzuna.shinyapps.io/niconico-search/" target="_blank" rel="noopener noreferrer"&gt;ニコニコ検索（仮）&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;公式のニコニコ動画では行えない、以下の検索が可能なのが特徴です。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;投稿日時、再生数、コメント数、マイリスト数、いいね数、再生時間をフィルタ条件に指定した検索&lt;/li&gt;
&lt;li&gt;検索結果を、コメント率、マイリスト率、いいね率、マイリスト数/コメント数の大小順で表示&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="使い方"&gt;使い方&lt;/h2&gt;
&lt;p&gt;こういう検索ができます。（マイリスト数/コメント数が大きい順に並べたいが、再生数やマイリスト数、コメント数が小さすぎるとマイリス数/コメ数が大きくなりすぎるので、下限を設定している）&lt;/p&gt;
&lt;img src="./image1.jpg" width="800px"&gt;
&lt;h2 id="作った動機"&gt;作った動機&lt;/h2&gt;
&lt;p&gt;公式では不可能な、概要に記載した検索方法やソート方法をやってみたかったからです。&lt;/p&gt;
&lt;p&gt;ジャンルによっては、良作の動画はマイリスト率が高かったり、&lt;a href="https://dic.nicovideo.jp/a/%E3%82%B3%E3%83%A1%E3%83%B3%E3%83%88%3C%E3%83%9E%E3%82%A4%E3%83%AA%E3%82%B9%E3%83%88" target="_blank" rel="noopener noreferrer"&gt;マイリスト数＞コメント数&lt;/a&gt;となっていたりすることが特徴であることが知られています。そのような動画を見つけるためにこのWebアプリを作ってみました。&lt;/p&gt;
&lt;h2 id="ロジック"&gt;ロジック&lt;/h2&gt;
&lt;p&gt;ニコニコ動画公式の&lt;a href="https://site.nicovideo.jp/search-api-docs/snapshot" target="_blank" rel="noopener noreferrer"&gt;スナップショット検索API v2&lt;/a&gt;を叩き、マイリス率などを計算して指定したソート順で並べています。1回のリクエストで最大100件まで取得できるため、検索結果が100件を超える場合は100件ずつ分けてスリープを挟んで全て取得してから指定したソート順で並べます。&lt;/p&gt;
&lt;p&gt;このように全て取得することでコメント率やマイリス率、コメ数/マイリス数のソートが可能になりますが、その代わり検索結果数が多くなると結果が返るまでに数十秒要します。&lt;/p&gt;
&lt;p&gt;Shinyで実装しており、shinyapps.ioでデプロイしています。&lt;/p&gt;
&lt;h2 id="今後やりたいこと"&gt;今後やりたいこと&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;独自ドメイン化
&lt;ul&gt;
&lt;li&gt;折角ドメインを取ったので&lt;/li&gt;
&lt;li&gt;shinyapps.ioで独自ドメインを使うには299ドル/月のProfessionalコースに入らないといけないようなのでHerokuへの移植を検討中&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>RMarkdownにhighlight.jsのcssテーマを適用する</title><link>https://suzunano.net/posts/rmarkdown-highlightjs/</link><pubDate>Thu, 18 Feb 2021 00:00:00 +0900</pubDate><guid>https://suzunano.net/posts/rmarkdown-highlightjs/</guid><description>&lt;h2 id="rmarkdownにhighlightjsのcssテーマを適用する"&gt;RMarkdownにhighlight.jsのcssテーマを適用する&lt;/h2&gt;
&lt;p&gt;highlight.jsのcssテーマを用いてRMarkdownのコードハイライトを変更する方法を示します。&lt;/p&gt;
&lt;p&gt;以下の記事にあるように、RMarkdownはYAML部分のオプションを用いてコード部分のハイライトを変更することができます。設定できるコードハイライトのテーマはdefault,tango,pygments,kate,monochrome,espresso,zenburn,haddock,textmateの9個です。&lt;/p&gt;
&lt;p&gt;&lt;a href="https://qiita.com/kazutan/items/ca20f26fba3f6fba81c5" target="_blank" rel="noopener noreferrer"&gt;R Markdownでコードハイライトのテーマ設定&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;もっと多くのハイライトテーマを使うには、例えばhighlight.jsを用いる方法があります。2021年2月のバージョン10.5.0時点で97パターンあります。&lt;/p&gt;
&lt;p&gt;そこで、&lt;a href="https://blog.atusy.net/2019/04/22/rmd-line-num-with-highlightjs/" target="_blank" rel="noopener noreferrer"&gt;highlightjs と highlightjs-line-numbers プラグインで Rmarkdown のコードブロックに行番号をつける&lt;/a&gt;を参考にしながら、highlight.jsのcssテーマをRMarkdownのコードハイライトに当てる方法を考えました。&lt;/p&gt;
&lt;p&gt;結論としては、以下のコードをRMarkdownのYAML部分の直下（＝本文部分の先頭）に記載します。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;style type=&amp;quot;text/css&amp;quot;&amp;gt;
@import &amp;quot;https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.5.0/styles/rainbow.min.css&amp;quot;;
&amp;lt;/style&amp;gt;
```{css, echo=FALSE}
pre{
border: transparent;
background: transparent;
padding: 0px;
}
/* preのpadding 9.5px + border 1px */
code.hljs{
padding: 10.5px;
}
```
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;@importで読み込むhighlight.jsのCDNのアドレスの最後の&amp;quot;rainbow.min.css&amp;quot;の&amp;quot;rainbow&amp;quot;は、&lt;a href="https://github.com/highlightjs/highlight.js/tree/master/src/styles" target="_blank" rel="noopener noreferrer"&gt;highlight.jsのGitHubのリポジトリ&lt;/a&gt;にある&amp;quot;テーマ名.css&amp;quot;の&amp;quot;テーマ名&amp;quot;です。&lt;a href="https://highlightjs.org/static/demo/" target="_blank" rel="noopener noreferrer"&gt;highlight.jsのdemo&lt;/a&gt;で使いたいテーマを見つけたら、このリポジトリからテーマ名を探します。CDNで読み込むときはテーマ名に.minを付けます。&lt;/p&gt;
&lt;p&gt;RMarkdownのコードブロックでは、border: 1px, padding: 9.5pxのpreという要素の中に、code.hljsという要素のコード部分が存在します。上のコードの最初の3行（style～/style部分）だけをRMarkdownに書いてcssを変更しないと、code.hljsはテーマの背景に変わっているのに、borderとpaddingはデフォルトのグレーのまま残ってしまいます。highlight.jsのcssはcode.hljsにかかるので、上のようにpreのborderとbackgroundをcode.hljsの透過としてpaddingをいじることで、見た目をうまく調節しています。&lt;/p&gt;
&lt;p&gt;なお、上記ではborderをtransparentにしているので、背景が白系のテーマだと、コードとそれ以外のブロックの区別が付きません。その場合はborder: transparent;を削除すると、枠線だけ残ります。&lt;/p&gt;
&lt;h2 id="おまけフォント変更"&gt;おまけ（フォント変更）&lt;/h2&gt;
&lt;p&gt;以下を追加することで、RMarkdownのコード部分のフォントをコード用のフォントに変えられます。インラインのコードのフォントも変わります。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;```{css, echo=FALSE}
code{
font-family: SFMono-Regular, Consolas, Liberation Mono, Menlo, monospace;
}
```
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;RStudioのデフォルトのフォントにする場合は以下です。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;```{css, echo=FALSE}
code{
font-family: Lucida Console, monospace;
}
```
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id="環境"&gt;環境&lt;/h2&gt;
&lt;p&gt;Rは4.0.2、RMarkdownは2.7、highlight.jsは10.5.0のバージョンを使用しています。&lt;/p&gt;</description></item><item><title>RMeCabで形態素解析した結果をtidy textなdata.frameで取得する</title><link>https://suzunano.net/posts/rmecab-tidy-text-result/</link><pubDate>Mon, 08 Feb 2021 00:00:00 +0900</pubDate><guid>https://suzunano.net/posts/rmecab-tidy-text-result/</guid><description>&lt;p&gt;RMeCabを使っていると、品詞や品詞細分類、読みなどの結果をdata.frameの形で取得したいと思うことがあります。ここでは、品詞についての全ての結果と、品詞情報のみをdata.frameで取得する方法を示します。&lt;/p&gt;
&lt;p&gt;例として、以下のtextを形態素解析します。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-r"&gt;library(tidyverse)
library(magrittr)
library(RMeCab)
text &amp;lt;- c(&amp;quot;吾輩は猫である。&amp;quot;,&amp;quot;名前はまだない。&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id="全結果を取得する場合"&gt;全結果を取得する場合&lt;/h2&gt;
&lt;p&gt;以下のように、RMeCabText関数は、一つ一つの形態素ごとに「表層形」、「品詞」、「品詞細分類1」、「品詞細分類2」、「品詞細分類3」、「活用形1」、「活用形2」、「原形」、「読み」、「発音」の長さ10のベクトルを要素に持つリストを作ります。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-r"&gt;tmp &amp;lt;- tempfile()
write(text[1],tmp)
rmecab_text &amp;lt;- RMeCabText(tmp)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class="language-r"&gt;rmecab_text %&amp;gt;%
head(3)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;## [[1]]
## [1] &amp;quot;吾輩&amp;quot; &amp;quot;名詞&amp;quot; &amp;quot;代名詞&amp;quot; &amp;quot;一般&amp;quot; &amp;quot;*&amp;quot; &amp;quot;*&amp;quot;
## [7] &amp;quot;*&amp;quot; &amp;quot;吾輩&amp;quot; &amp;quot;ワガハイ&amp;quot; &amp;quot;ワガハイ&amp;quot;
##
## [[2]]
## [1] &amp;quot;は&amp;quot; &amp;quot;助詞&amp;quot; &amp;quot;係助詞&amp;quot; &amp;quot;*&amp;quot; &amp;quot;*&amp;quot; &amp;quot;*&amp;quot; &amp;quot;*&amp;quot; &amp;quot;は&amp;quot;
## [9] &amp;quot;ハ&amp;quot; &amp;quot;ワ&amp;quot;
##
## [[3]]
## [1] &amp;quot;猫&amp;quot; &amp;quot;名詞&amp;quot; &amp;quot;一般&amp;quot; &amp;quot;*&amp;quot; &amp;quot;*&amp;quot; &amp;quot;*&amp;quot; &amp;quot;*&amp;quot; &amp;quot;猫&amp;quot; &amp;quot;ネコ&amp;quot; &amp;quot;ネコ&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;なお、RMeCabTextはファイルから読み込むため、R上のオブジェクトをRMeCabTextに掛ける場合はtempfileで一時ファイルを作ってそれを読み込む形をとります。&lt;/p&gt;
&lt;p&gt;ということは、textの各要素についてRMeCabTextを行い、各結果のリストをflatten_chrして全部繋げてから10列のdata.frameにすれば欲しい結果が得られます。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-r"&gt;res1 &amp;lt;- text %&amp;gt;%
map(function(x){
tmp &amp;lt;- tempfile()
write(x,tmp)
func &amp;lt;- quietly(RMeCabText)
res &amp;lt;- func(tmp)$result
res_df &amp;lt;- res %&amp;gt;%
flatten_chr() %&amp;gt;%
matrix(ncol=10,byrow=TRUE) %&amp;gt;%
as.data.frame() %&amp;gt;%
set_colnames(c(&amp;quot;surface&amp;quot;,&amp;quot;pos&amp;quot;,&amp;quot;pos1&amp;quot;,&amp;quot;pos2&amp;quot;,&amp;quot;pos3&amp;quot;,&amp;quot;form1&amp;quot;,&amp;quot;form2&amp;quot;,&amp;quot;base&amp;quot;,&amp;quot;yomi&amp;quot;,&amp;quot;hatsuon&amp;quot;))
file.remove(tmp)
return(res_df)
}) %&amp;gt;%
# textの何番目の要素を形態素解析したかというidを付けておく
enframe(name=&amp;quot;id&amp;quot;,value=&amp;quot;value&amp;quot;) %&amp;gt;%
unnest(value) %&amp;gt;%
# tibbleをdata.frameに直す（直さなくてもいい）
as.data.frame()
res1
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;## id surface pos pos1 pos2 pos3 form1 form2 base yomi
## 1 1 吾輩 名詞 代名詞 一般 * * * 吾輩 ワガハイ
## 2 1 は 助詞 係助詞 * * * * は ハ
## 3 1 猫 名詞 一般 * * * * 猫 ネコ
## 4 1 で 助動詞 * * * 特殊・ダ 連用形 だ デ
## 5 1 ある 助動詞 * * * 五段・ラ行アル 基本形 ある アル
## 6 1 。 記号 句点 * * * * 。 。
## 7 2 名前 名詞 一般 * * * * 名前 ナマエ
## 8 2 は 助詞 係助詞 * * * * は ハ
## 9 2 まだ 副詞 助詞類接続 * * * * まだ マダ
## 10 2 ない 形容詞 自立 * * 形容詞・アウオ段 基本形 ない ナイ
## 11 2 。 記号 句点 * * * * 。 。
## hatsuon
## 1 ワガハイ
## 2 ワ
## 3 ネコ
## 4 デ
## 5 アル
## 6 。
## 7 ナマエ
## 8 ワ
## 9 マダ
## 10 ナイ
## 11 。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;RMeCabTextは読み込んだファイルパスをコンソールに出力します。これはありがたいのですが、今回読み込んでいるのは一時ファイルであり、しかもtextの1要素ずつ一時ファイルを作っているためにコンソールの出力がすごい量になるので、purrr::quietlyを用いて出力しないようにしています。&lt;/p&gt;
&lt;p&gt;今形態素解析にかけたtextはベクトルでしたが、実際の分析では以下のようなdata.frameの場合もよくあります。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-r"&gt;df &amp;lt;- data.frame(sentence_id=1:2,text=text)
df
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;## sentence_id text
## 1 1 吾輩は猫である。
## 2 2 名前はまだない。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;その場合でも、一発でtext列と紐付いた結果が得られますね。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-r"&gt;left_join(df,res1,by=c(&amp;quot;sentence_id&amp;quot;=&amp;quot;id&amp;quot;)) %&amp;gt;%
head(3)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;## sentence_id text surface pos pos1 pos2 pos3 form1 form2 base
## 1 1 吾輩は猫である。 吾輩 名詞 代名詞 一般 * * * 吾輩
## 2 1 吾輩は猫である。 は 助詞 係助詞 * * * * は
## 3 1 吾輩は猫である。 猫 名詞 一般 * * * * 猫
## yomi hatsuon
## 1 ワガハイ ワガハイ
## 2 ハ ワ
## 3 ネコ ネコ
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;このような綺麗な形式のdata.frameが得られると、その後の分析が楽になりますね。&lt;/p&gt;
&lt;h2 id="語と品詞だけあればよい場合"&gt;語と品詞だけあればよい場合&lt;/h2&gt;
&lt;p&gt;品詞細分類などの列は不要であり、形態素解析された語と品詞の列だけあれば十分という場合も多いです。&lt;/p&gt;
&lt;p&gt;この場合、上記のコードを実行後にselectで必要な列のみ選択してもいいのですが、以下のRMeCabCを用いる方法もあります。&lt;/p&gt;
&lt;p&gt;RMeCabCはベクトルを引数に取り、以下のような返り値を返します。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-r"&gt;RMeCabC(text[1]) %&amp;gt;%
head(3)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;## [[1]]
## 名詞
## &amp;quot;吾輩&amp;quot;
##
## [[2]]
## 助詞
## &amp;quot;は&amp;quot;
##
## [[3]]
## 名詞
## &amp;quot;猫&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;ということは、flatten_chrすれば、分かち書きされた結果のベクトルに、品詞情報が名前として付いた名前付きベクトルが得られるので、以下のようにすれば欲しい結果が得られます。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-r"&gt;res2 &amp;lt;- text %&amp;gt;%
map(function(x){
mecab_raw &amp;lt;- RMeCabC(x)
mecab_vec &amp;lt;- flatten_chr(mecab_raw)
mecab_df &amp;lt;- data.frame(surface=mecab_vec,pos=names(mecab_vec))
return(mecab_df)
}) %&amp;gt;%
enframe(name=&amp;quot;id&amp;quot;,value=&amp;quot;value&amp;quot;) %&amp;gt;%
unnest(value) %&amp;gt;%
as.data.frame()
res2 %&amp;gt;%
head
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;## id surface pos
## 1 1 吾輩 名詞
## 2 1 は 助詞
## 3 1 猫 名詞
## 4 1 で 助動詞
## 5 1 ある 助動詞
## 6 1 。 記号
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;形態素解析にかけたいtextがdata.frameの形式の場合でも、先の例と同様にすれば結果が得られます。&lt;/p&gt;
&lt;p&gt;purrr様々ですね。&lt;/p&gt;
&lt;h2 id="tidy-text"&gt;tidy text&lt;/h2&gt;
&lt;p&gt;tidy textという概念があります。tidy textについては以下の書籍が詳しいです。&lt;/p&gt;
&lt;p&gt;&lt;a href="https://www.amazon.co.jp/R%E3%81%AB%E3%82%88%E3%82%8B%E3%83%86%E3%82%AD%E3%82%B9%E3%83%88%E3%83%9E%E3%82%A4%E3%83%8B%E3%83%B3%E3%82%B0-%E2%80%95tidytext%E3%82%92%E6%B4%BB%E7%94%A8%E3%81%97%E3%81%9F%E3%83%87%E3%83%BC%E3%82%BF%E5%88%86%E6%9E%90%E3%81%A8%E5%8F%AF%E8%A6%96%E5%8C%96%E3%81%AE%E5%9F%BA%E7%A4%8E-Julia-Silge/dp/4873118301" target="_blank" rel="noopener noreferrer"&gt;Rによるテキストマイニング ―tidytextを活用したデータ分析と可視化の基礎&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;これは、以下の英語の原文を和訳したものです。&lt;/p&gt;
&lt;p&gt;&lt;a href="https://www.tidytextmining.com/" target="_blank" rel="noopener noreferrer"&gt;Text Mining with R&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;原文の第1章冒頭で、tidy textは以下のように定義されています。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&amp;ldquo;We thus define the tidy text format as being a table with one-token-per-row. A token is a meaningful unit of text, such as a word, that we are interested in using for analysis, and tokenization is the process of splitting text into tokens.&amp;rdquo;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;1行につき1トークン（単語や形態素など）のdata.frameの形でトークンが記載されているデータのことです。この記事で紹介した方法では、形態素解析の結果がtidy textな形式で得られるので、その後のデータの加工が容易になるというメリットがあります。&lt;/p&gt;</description></item></channel></rss>