盆暗の学習記録

データサイエンス ,エンジニアリング,ビジネスについて日々学んだことの備忘録としていく予定です。初心者であり独学なので内容には誤りが含まれる可能性が大いにあります。

React+TypeScript+ViteでChrome拡張機能を作るときの構成

React + TypeScript + ViteでGoogle Chrome拡張機能を作る方法をメモしておきます。

なお、以下の環境下での話になります。

// 環境
"vite": "^5.2.0",
"typescript": "^5.2.2"

TypeScriptでプロジェクトを作る

まずプロジェクトを作成します(ここはViteのドキュメント通り)

npm create vite@latest myapp -- --template react-ts

@types/chromeを入れる

chrome API(例えばchrome.i18nchrome.tabsなど)を使う場合、chrome オブジェクトの型定義がなくてエラーになるので@typesをインストールします(TSではなくJSで作るなら不要です)。

npm i @types/chrome

manifest.jsonを追加する

viteのデフォルトの設定ではpublic/ディレクトリのファイルはビルド時にトランスパイルされずにdist/に同梱されますので、manifest.jsonpublic/に置きます。

マルチページ構成にする

拡張機能の開発では複数のhtmlを生成したい場合があります。例えばアイコンをクリックしたときに出てくるpopup.htmlやオプションの設定画面options.htmlといった具合です。その場合はviteをマルチページアプリにすると対応できます。

例えばsrcの中にoptionsとpopupという2つのディレクトリをつくり、それぞれにindex.html等を入れるとします。

src
├── options
│   ├── App.tsx       
│   ├── index.html    
│   └── main.tsx      
├── popup
│   ├── App.tsx       
│   ├── index.html    
│   └── main.tsx      
└── vite-env.d.ts 

vitre.config.tsでそれぞれのindex.htmlを参照するように設定を変えます。

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { resolve } from 'path'

const outDir = resolve(__dirname, 'dist')

export default defineConfig({
  plugins: [react()],
  build: {
    outDir: outDir,
    rollupOptions: {
      input: {
        popup: resolve(__dirname, 'src/popup/index.html'),
        option: resolve(__dirname, 'src/options/index.html'),
        background: resolve(__dirname, 'src/background.ts')
      }
    },
  },
})

もしここでCannot find name '__dirname'.ts(2304)のようなエラーが出ている場合はnodeのtypesを入れます。

npm i @types/node

また上記のconfigのもとでは、npm run buildしたときのhtmlファイルは

dist
├── assets
│   ├── background-B9WwJWnc.js
│   ├── client-CYooLA_G.js    
│   ├── option-DptWORB2.js    
│   └── popup-CQLN0kRt.js
└── src
    ├── options
    │   └── index.html
    └── popup
        └── index.html

という感じに生成されるようになります。

そのため、manifest.jsonをdistディレクトリに置いて拡張機能のパッケージにする場合、manifest.json

{
  "manifest_version": 3,
    ...
  "action": {
    "default_popup": "./src/popup/index.html"
  },
  "options_page": "./src/options/index.html"
}

というふうにそれぞれのhtmlを参照するようにしてやればよい、ということになります。

なお、npm run devで起動するdev環境ではそれぞれ

http://localhost:5173/src/options/index.html
http://localhost:5173/src/popup/index.html

というパスで確認できるようになります。

トランスパイル後もファイル名を維持するようにする

もし、バックグラウンドで動かす background.jsのようなものを作りたい場合、デフォルトのviteではビルド時にファイル名が書き換えられてしまうため、src/background.tsbackground-B9WwJWnc.jsのような[name]-[hash].js形式の名前になってしまいます。

そこでvite.config.tsrollupOptions.output.entryFileNames'[name].js'に設定することで名前が維持されるようになります。

rollupOptions: {
  ...
  input: {
    ...
    background: 'src/background.ts' // background.tsをビルドの対象に含める
  }
  output: {
    entryFileNames: '[name].js'  // ビルド後もファイル名を維持するようにする
  }
}

(参考:ビルドオプション | Vite

参考リポジトリ

上記を全部入れたコードを置いておきます

github.com

Linux OSの起動時にプログラムを自動実行する

最近よく使うので簡単にメモします。

私の環境

Ubuntu 20.04(WSL)

手順

/etcrc.local というファイルを作成する

sudo vim /etc/rc.local

rc.local の中身はシェルスクリプトで、ここに実行したいプログラムを書く。 (shebangも書かないとうまくいかないらしい)

#!/bin/bash

# 例: docker-composeの起動
cd /my/work/dir
sudo docker compose up -d

rc.local はrootにのみ全権限を付与

sudo chmod 700 /etc/rc.local

systemdへの登録

sudo systemctl start rc-local

その後

sudo systemctl status rc-local

でactiveになっていたら設定完了

WSLの場合

WSLの場合、ここで

System has not been booted with systemd as init system (PID 1). Can't operate. Failed to connect to bus: Host is down

というエラーがでることがあります。

この場合は、/etc/wsl.conf

[boot]
systemd=true

と書いてwslを再起動すればsystemdが動くようになるみたいです。

参考サイト

pythonで数値微分するときはnumdifftoolsが便利そう

scipy.optimize.approx_fprime

先日、フィッシャー情報量を対数尤度の2次の導関数から計算してみようと思い、ChatGPTに「pythonで数値微分するコードの例を出して」と尋ねてみました。

するとscipyの approx_fprime という関数が提案され、2次の導関数については approx_fprime を2回再帰的に適用する方法が提案されました。

これは例えば以下のように書くことができます。

import numpy as np
from scipy.optimize import approx_fprime
EPSILON = np.sqrt(np.finfo(float).eps)

def f(x):
    """微分したい対象の関数"""
    return np.sin(x)

def df(x):
    """1次の導関数"""
    return approx_fprime(x, f=f, epsilon=EPSILON)[0]

def ddf(x):
    """2次の導関数"""
    return approx_fprime(x, f=df, epsilon=EPSILON)[0]

ただ、これplotしてみると2階微分のほうはギザギザした曲線になるみたいです。

どうしてこうなるのかは approx_fprime の中身を知らないのでよくわからないのですが、とりあえず別の方法を探すことにしました。

numdifftools

numdifftools は数値微分をするためのパッケージで、 numdifftools.Derivative([対象の関数], n=[導関数の次数]) といった書き方で簡単に導関数を生成できます。

import numpy as np
import numdifftools as nd

def f(x):
    """微分したい対象の関数"""
    return np.sin(x)

df = nd.Derivative(f, n=1)
ddf = nd.Derivative(f, n=2)

これでプロットすると

と、滑らかな2次の導関数になっていることがわかります。

書籍の情報を検索して参考文献のフォーマットで取得できるChrome拡張機能を作ってみた

表題の通りのものを作りました

Book Searcher

もし同じニーズのある方がいらっしゃいましたらお使いください

背景

私は普段、なにか文献の情報を取得して、APA形式などの参考文献の形に整形したい場合は

Google Scholar ボタン

という、Googleが公式に出しているGoogle Scholar拡張機能を使っています。

この拡張機能Google Scholarなので論文を探すときには素晴らしいのですが、書籍の情報は検索しても出てこないことが多々あります。

そこで、もっとヒット率の高い拡張機能が欲しいと思って作ったのが今回の拡張機能になります。

機能

検索すると、画像のように検索結果の一覧がでてきます。

また、「Citation」のところをクリックすると参考文献フォーマットが出てきます。

どうやって作っているのか

書籍データ・検索システム

Google Books APIを使っています。

最初は別の方法を探したのですが、いいものが見つかりませんでした。 例えば版元ドットコムさんが出しているopenBDは良さそうなAPIかなーと思ったのですが、利用規約的に難しそうなのと、そもそも提供終了予定らしいので無理だなと。他の案だと、例えば国立国会図書館のデータは非営利なら使えそうな感じでしたが新しい本への網羅性など不安でしたので、結局「Google Booksがいいな」となりました。

もしGoogle BooksがScholarのように公式の拡張機能を出していたら自作する必要はなかったのですが…。

参考文献のフォーマットへの整形

citation-js というパッケージを使っています。

citation.js.org

最初はここのロジックも自作しようかと思っていたのですが、世の中にはいろいろなパッケージがあるのだなと痛感しました。ありがたいです。

ただ、日本語文献については専用の形式に整形するものを自前で作ってもよいかなーとも思っています。現状まだ対応できていないですが…

名前のパース

Google Books APIでの著者情報は姓名が分割されておらず、例えば Guido W. Imbens のようなフルネームが1つの文字列で返ってくる形になっています。

APA形式だとImbens, G. W.のようにしないといけませんので、著者の名前をパースする必要があります。

名前のパースはhumanparserパッケージを使っています。

www.npmjs.com

フロントエンド・UI

TypeScript で Vite + React の構成にしています。

またMaterial UIを使っています。

拡張機能の開発でReactを使ったのは初めて(これまではせいぜいjQueryくらいしか使わなかった)のですが、問題なく使えますね。

今後もこういう構成を使っていきたいと思います(Material UIはサイズ調整に苦労したので今後は採用しないかも)

既知の問題

Google Books APIの返り値において、日本語書籍の出版社の情報が欠損していることが結構あります。

そのような本は、この拡張機能では出版社の情報が抜けたままになっています。

今後いい解決策が思いついたら対応したいです。


その他のChrome拡張

他にもChrome拡張機能を出しているのでもしご興味があればぜひご覧ください

  • Link Generator:表示中のページへのリンクをお好みの形式で生成します(Markdown、HTMLなど)
  • note 目次追加:note.comの記事の右側に目次を追加します。

seabornのkdeplotでhue引数を使うときはcommon_norm=Falseを検討しよう

TL; DR

  • seaborn.kdeplotでクラスごとに分布を描くとき、デフォルト引数のままだとクラスごとのサンプル数が違うと分布の大きさも違ってしまう
  • kdeplotではデフォルトではcommon_norm=Trueになっており、全クラスの分布の面積の合計が1になるように分布が調整される
  • common_norm=Falseにするとクラスごとに分布を描いてくれるので、状況によっては設定したほうがいいかも

課題:クラスごとの分布の面積が違いすぎる

データの分布をノンパラ推定して図にしたいときにseabornのkdeplot系のメソッドを使う人は結構いると思います。hue引数を設定するだけでクラスごとに分布を描いてくれたりして便利ですよね。

# kdeplotのコード例
import seaborn as sns
sns.kdeplot(data=df, x="x", hue="class")

ただ、hueに指定しているクラスごとのサンプル数に偏りがあるとき(例えば、一方のクラスはサンプル数が多いけど他方は少ない、みたいなとき)にシンプルにkdeplotを描くと面積がサンプル数に応じたものになります。

上の図はどちらのclassも分散が1の正規分布で、平均が少し違うだけです。データ生成時のサンプル数に差をつけてsns.kdeplot(data=df, x="x", hue="class")のように描くと、図のようにサンプル数に応じて分布の大きさが変わってしまいます。

▶(参考)データ生成時のコード

import numpy as np
import pandas as pd

n = 10000
np.random.seed(0)
df = pd.concat([
    pd.DataFrame({"class": "A", "x": np.random.normal(loc=1, size=n)}),
    pd.DataFrame({"class": "B", "x": np.random.normal(loc=1.5, size=n//2)}),
])

しかし、カーネル密度推定自体は確率密度関数の推定をするのでサンプル数に応じて密度や分布の面積が変わってくるのは奇妙です。 実際、matplotlibとscipyを使って自分でKDEを行ってkdeplotのようなものを描けば、クラスごとのサンプル数が不均衡でも面積の大きさに大差のない図ができます。

▶(参考)kdeしてplotするコード

import matplotlib.pyplot as plt
from scipy.stats import gaussian_kde

x_range = np.linspace(-3, 6, 100)
fig, ax = plt.subplots()

for class_label in df["class"].unique():
    kernel = gaussian_kde(df.query(f"`class` == '{class_label}'")["x"])
    ax.plot(x_range, kernel(x_range), label=class_label)
ax.legend()
ax.set(xlabel="x", ylabel="Density")
fig.show()

調べてみたところ、サンプル数に応じて分布の大きさが変わるのはkdeplotの機能のようです。

対処法:common_norm=Falseにしよう

kdeplotにはcommon_normという引数があります。

common_norm : bool

If True, scale each conditional density by the number of observations such that the total area under all densities sums to 1. Otherwise, normalize each density independently.

(True の場合、すべての密度の総面積の合計が 1 になるように、各条件付き密度を観測値の数でスケールします。それ以外の場合は、各密度を個別に正規化します。)

デフォルトではTrueになっているcommon_normという引数で、複数のクラスごとの分布を出している場合もそれらの全体の面積の合計が1となるように正規化をかけているそうです。

なのでcommon_norm=Falseに設定すれば各クラスの密度の面積の合計が1になるように密度のスケールを扱ってくれます。

# common_normをFalseにすればよい
import seaborn as sns
sns.kdeplot(data=df, x="x", hue="class", common_norm=False)

状況に応じて引数を使い分けるべきでしょうが、クラスごとに分布の形状を比較したいときはcommon_norm=Falseにしたほうがよさそうに思えます。

Plotlyで地図上に散布図を描く

ドラッグしたりズームできるようなマップ上に散布図などを描きたいとき、FoliumだけでなくPlotlyも使えることを知ったのでメモしておきます。

環境

python:3.11のDocker Imageの下で、以下のバージョンのライブラリで試しました。

jupyterlab==4.0.10
plotly==5.18.0
pandas==2.1.4

以前のPlotlyはJupyterLabで使うとうまく表示されなくて、jupyter extensionや別のライブラリを追加したりといろいろ対策する必要があった記憶があるのですが、上記のバージョンでは何もせずとも問題なく描画できました。

データの用意

とりあえずいくつか点をplotできればいいので適当にマンションのデータを作ってみます。

import pandas as pd

df = pd.DataFrame([
    dict(name="麻布台ヒルズ ガーデンプラザレジデンス", address="東京都港区虎ノ門5-9-1", lat=35.662242, lon=139.742654, price=900_000, size=80),
    dict(name="六本木ヒルズレジデンス A棟", address="東京都港区六本木6丁目12-1", lat=35.658884, lon=139.728583, price=600_000, size=60),
    dict(name="六本木ヒルズレジデンス B棟", address="東京都港区六本木6丁目12-2", lat=35.658869, lon=139.729598, price=650_000, size=65),
    dict(name="六本木ヒルズレジデンス C棟", address="東京都港区六本木6丁目12-3", lat=35.658783, lon=139.730071, price=860_000, size=80),
    dict(name="六本木ヒルズレジデンス D棟", address="東京都港区六本木6丁目12-4", lat=35.658901, lon=139.731209, price=400_000, size=80),
    dict(name="六本木さくら坂レジデンス", address="東京都港区六本木6丁目16-6", lat=35.658113, lon=139.729766, price=1_700_000, size=200),
    dict(name="元麻布ヒルズ", address="東京都港区元麻布1丁目3-1", lat=35.653843, lon=139.731913, price=750_000, size=80),
]).assign(
    price_per_size = lambda self: (self.price / self.size).round(1)
)

緯度経度の情報がある手頃なデータが思いつかなかったので、MORI LIVINGさんのマンションからいくつか選び出し、Geocodingさんで住所を緯度経度に変換しました。

色の塗り分け用にそれっぽい価格もつけました。価格はモダンスタンダードさんの参考賃料を参考にそれっぽい値を入れています。

描いてみる

plotly.express.scatter_mapbox()で描くことができるようです

デフォルトだとMapBoxのAPIトークンが必要になりますが、mapbox_style='open-street-map'にすればトークンは不要です。

import plotly.express as px

fig = px.scatter_mapbox(
    df,
    lat="lat", lon="lon",
    zoom=14, height=300,
    # ホバーする内容の設定
    hover_name="name",
    hover_data=["name", "address", "price", "size"],
    # 色の塗り分けの設定 (Blues[5:]は濃いめの色のみに絞っている)
    color = "price_per_size",
    color_continuous_scale=px.colors.sequential.Blues[5:],
)
fig.update_layout(mapbox_style='open-street-map')
fig.update_layout(margin={"r":0,"t":0,"l":0,"b":0})
fig.show()

ただ、これだと地図の情報が多すぎて散布図が見づらい…。別のスタイルも試してみます。

carto-positroncarto-darkmatter の2つはかなりシンプルで、特にcarto-positronは見やすくていい感じでした。

import plotly.express as px

styles = ['carto-positron', 'carto-darkmatter']
for style in styles:
    print(f"mapbox_style={style}")
    fig = px.scatter_mapbox(
        df,
        lat="lat", lon="lon",
        zoom=14, height=300,
        # ホバーする内容の設定
        hover_name="name",
        hover_data=["name", "address", "price", "size"],
        # 色の塗り分けの設定 (Blues[5:]は濃いめの色のみに絞っている)
        color = "price_per_size",
        color_continuous_scale=px.colors.sequential.Blues[5:],
    )
    fig.update_layout(mapbox_style=style)
    fig.update_layout(margin={"r":0,"t":0,"l":0,"b":0})
    fig.show()

自由にズームしたり、ホバーしたときに他の変数の値も見られるようにできるので、データの確認をするのに良さそうな感じです。

Rを使っていたときはplotlyをよく使っていたのですが、Pythonではまだまだ使いこなせてないので(Dashとかも)、この本も参考にしつつ勉強してみようかとおもいます(Plotlyを深く解説してるほぼ唯一の本)。

stliteを試してみる

Streamlitを静的サイトで動かせるstliteというライブラリがあるらしいです。

github.com

簡単に試してみたのでメモしておきます。

Streamlitとは

StreamlitPythonだけで簡単にWebアプリを作るライブラリです。

よくある使い方としてはダッシュボードとして、ユーザーの入力に応じてフィルタリングができる動的なグラフや表の表示などに使われます。

Rでいうと{shiny}パッケージのようなものです。

例えば、

import streamlit as st

name = st.text_input('Your name')
st.write("Hello,", name or "world")

というコードを書いて(仮にtest_streamlit.pyというファイル名にするとします)、シェルで

$ streamlit run test_streamlit.py

を実行するとローカルでサーバが建てられて、streamlitのアプリにlocalhostからアクセスできるようになります。

上記のコードだと、次の画像のようなものが出来上がります。

stliteとは

stliteはStreamlitを静的サイト(例えば1つのhtmlファイル)で動かせるようにするものです。

WebAssemblyを用いてクライアントサイドでPythonを動かすPyodideを使っているらしいです。

例えば、次のようなhtmlファイルを作ります(※このコードは公式のサンプルコードからお借りしました)。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
    <title>stlite app</title>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@stlite/mountable@0.39.0/build/stlite.css" />
  </head>
  <body>
    <div id="root"></div>
    <script src="https://cdn.jsdelivr.net/npm/@stlite/mountable@0.39.0/build/stlite.js"></script>
    <script>
      stlite.mount(
        `
import streamlit as st
name = st.text_input('Your name')
st.write("Hello,", name or "world")
`,
        document.getElementById("root")
      );
    </script>
  </body>
</html>

上記htmlでは、CDNからstliteを読み込んでいます。そしてstlite.mount()の部分に入れたPythonコードが実行され、streamlitが表示されます。

このhtmlファイルをブラウザで開くと、次の画像のような画面が表示されます。

最初に数秒の読み込みが入りますが、最終的には同様のstreamlitアプリが表示されています。

他の使用例

APIを呼び出す簡単なアプリを試してみようと思い、天気予報 API(livedoor 天気互換)を使って指定されたエリアの天気予報を表示するアプリを作ってみました。

使用したコードは以下のものになります。

import streamlit as st
import json
from pyodide.http import open_url # NOTE: stliteではrequestsやurllibは使えないためこちらを使う

AREA_ID_DICT = {'稚内': '011000', '旭川': '012010',...} # 長いので一部のみ載せています

def get_weather(area_id):
    url = f"https://weather.tsukumijima.net/api/forecast?city={area_id}"
    res = open_url(url)
    return json.loads(res.read())


area = st.selectbox(label="地域", options=list(AREA_ID_DICT.keys()), index=45)
area_id = AREA_ID_DICT[area]
weather = get_weather(area_id)

st.divider()
st.header(f'{weather["title"]}')

forecast = weather["forecasts"][1]
st.markdown(f'**{forecast["dateLabel"]}**')
min_temp = forecast["temperature"]["min"]["celsius"]
max_temp = forecast["temperature"]["max"]["celsius"]

col1, col2 = st.columns(2)
col1.metric(label="最高気温", value=f"{max_temp} ℃")
col2.metric(label="最低気温", value=f"{min_temp} ℃")
st.divider()
st.markdown(f'{weather["description"]["bodyText"]}')

なおAREA_ID_DICTは天気予報APIのエリア指定に必要なIDを入れているものです。長いので本記事では一部のみ掲載します。元のデータはこちらのXMLです。

stliteはrequestsurllibが使えないようなのでAPI呼び出しのときは少し注意が必要そうです。stliteのリポジトリlimitationsの節にも次のように書いてあります。

For URL access, urllib and requests don't work on Pyodide/stlite, so we have to use alternative methods provided by Pyodide, such as pyodide.http.pyfetch() or pyodide.http.open_url().

所感

Streamlitは便利である一方でPythonを動かすバックエンド(サーバー)が必要なのは地味にネックだと思いますので(StreamlitのCommunity Cloudを使って良い状況ならそれで問題ないですが)、stliteはそこのハードルが減っていて非常に良さそうだなと思いました。

またhtmlファイルだけを共有する方法もとれるので、ホスティングするほどじゃないちょっとしたダッシュボードやアプリを共有するのにも一応使えるかなと思いました。

例えば、「社内プロダクト用に作ったAPIがあって、ビジネスサイドの人が『自分の業務フロー中でも使ってみたい』と言ってくれた、でもその人はコードが書けないからGUIのあるアプリを渡したい・・・」みたいな状況で、その人の業務フローに合わせてAPIを呼び出すStreamlitの簡単なアプリを作ってhtmlファイルに埋め込んで渡してあげてPoCする、みたいなことに使えそうです。

なかなかおもしろい技術だと感じました。