右クリック長押し+マウス操作でスクロール(macOS)

前置き

マウスホイールを備えていないトラックボール等でスクロールを容易に行うためのカスタマイズとして右クリック長押し状態での操作をスクロールに割り当てるというものがある。

HammerspoonというmacOS用のオートメーションツールを使ってこれを実現している例があり、Webを探すといくつか記事が見つかる。 おそらくKarabinerのissueに付けられたコメント(Help Enabling virtual scroll wheel on Sierra. · Issue #814 · pqrs-org/Karabiner-archived)が最初で、どれもこれを参照していたりこれと同様の設定している。元は2017年のコメントだが、現在のHammerspoonにそのまま設定しても概ね問題なく動作する。

このスクリプトによるスクロール操作はかなりスムーズで反応も良く気に入っている。例えばKarabinerでもmouse_motion_to_scrollという設定(mouse_motion_to_scroll | Karabiner-Elements)を使うことで似たようなことが可能だが、それに比較するとスムーズさが段違いである。

ただ、多少気になる点があったため若干の修正をした。

準備

Hammerspoonをインストールして起動しておく。

やること

以下の設定をHammerspoonの設定ファイルに追加する。設定ファイルはOpen Configで開き、Reload Configで反映する。

右クリック長押し+トラックボール操作でスクロールする

説明

当初のスクリプトに対して3点ほど修正している。

  1. hs.mouse.setAbsolutePosition, hs.mouse.getAbsolutePositionhs.mouse.absolutePositionに差し替え。
    • deprecatedなので。動作には影響なし。
  2. デバウンス処理の追加
    • 最近使っているcocot38mini(cf. cocot38miniのキーマップ設定 - もうカツ丼はいいよな)やpicot O44と特定のアプリ(例えばVisual Studio Code)の組み合わせだと、スクロール挙動が非常に重くなる場合がある。
    • ログを仕込んだりして様子をみた感じ、スクロールイベントが短時間に非常に多く発火しているのが原因のように思われたので、イベントの発生間隔に制限を加えて対処した。
    • 今のところうまく動いている。
  3. モディファイアキー対応
    • 一部のアプリはCmd+スクロールが拡大倍率の調整などに割り当てられている事がある(Adobe XDなど)。
    • 元のスクリプトではCmd含むモディファイアキーの押下が反映されなかった。
    • hs.eventtap.event.newScrollEventのドキュメント(Hammerspoon docs: hs.eventtap.event)を確認したところ、モディファイアキーを受け付ける引数があったのでそれを設定できるようにした。

cocot38miniのキーマップ設定

昨年の8月ごろから自作キーボードに手を出して、最初はlily58 Pro、その後しばらくはKeyball44を使い、今はcocot38mini(cocot38mini – cocotkeebs)を主に使っている。

cocot38miniは30%と呼ばれるサイズのキーボードで、数字キーに加えてShiftやTabなどのモディファイアキーも配置する余裕がないので、キーマップをどうするかが特に問題になる。しばらくの試行錯誤を経てだいぶ使えるようになってきたので設定を公開しておく。

キーボード構成

cocot38mini(※VとCの位置は意図的ではなく間違え)

キーマップ

標準で8レイヤー使うことができ、7つ使用している。 レイヤーは多めに使っているが基本的には標準的なキー配列から極端に逸脱しないような方針で設定している。

レイヤー0

レイヤー0

  • 基本的にモディファイアキーは標準のキーのホールドに割り当てるような形。
  • Aに割り当ててあるレイヤー3はcontrolに近い役割をするように設定してある。
  • TabはQとWを同時押しすると発火するようにComboで設定してある。
  • 中央のキーと親指右下のキーは余ったのでDevtools起動とスクリーンショット撮影用のショートカット、マクロを割り当ててある。

レイヤー1

レイヤー1

  • 基本的にはCmd+何らかのキーを発火するようなレイヤーとしてある。
  • あまり用途がない部分は記号を割り当ててある。
    • JYENは\、|を入力する。
  • Z部分のTap DanceはHoldをShift、TapをCmd+Zに割り当ててある。
    • こうしないとレイヤーに入ってからShiftを入力する方法がない。
    • 素早く入力してしまうとShiftにならないのが若干課題。Taping Termを短くすれば素早い入力には対応できるが、今度はCmd+Zが入力しづらくなる。
  • 標準レイヤー以外のロータリーエンコーダはブラウザなどの拡大縮小、押し込みで倍率リセットに設定している。

レイヤー2

レイヤー2

  • 数字、ファンクションキー、カーソル移動用。
  • カーソルはVimでの操作が馴染んでいるのでhjklに割り当て。
  • :を置いてあるのは時刻入力の際に便利なので。

レイヤー3

レイヤー3

  • Control+何らかのキーを発火するようなレイヤーとしてある。
  • Controlと組み合わせての操作をするのは基本的にEmacsキーバインドで操作をしたい場合なので、標準のキーバインドで代替できる場合は標準のキーバインドを設定している。
    • macOSの場合、多くのアプリでEmacsキーバインドによる操作は受け付けてもらえるが、アプリによっては操作できないことがあるので、標準のキーバインドで設定しておいたほうが汎用性が高くなる。
  • ControlをAと共存させる場合、Control+A(行頭に移動)をどう入力するかが課題になるが、運指が似ているA+Sを割り当てることで対応した。
    • この場合Control+Sが入力できないが、Control+SをEmacsのように検索のショートカットとして受け付けてくれるアプリはそれほど多くなく、自分もほとんど使用していなかったので今のところ不便はない。

レイヤー4

レイヤー4

  • デフォルトでAuto mouse layerに割り当てられているので、トラックボールを操作すると自動的にこのレイヤーになる。
  • 基本的にはマウスボタンを割り当ててあるが、右手部分には多少ショートカットを配置している。一部はOS側の設定も組み合わせている。
    • Ctrl+]はSpotlight起動用。
    • Ctrl+Cmd+Iは通知センター起動用。
    • 0xc1はmacOSの場合Misson Controlを起動するキーコードになっている。
    • LとHはブラウザ拡張(Surfingkeys)でブラウザの進むと戻るに割り当てているもの。
  • スクロールに対応するボタンは割り当てていないが、これは別途Hammerspoonのスクリプトにより右クリック押しっぱなしでスクロールできるような設定をしてある
  • 左手側はトラックボール操作時には使わないので、設定変更用のキーなどを適当に置いてある。

レイヤー5、6

レイヤー5
レイヤー6

  • 配置しきれない記号を置いてある。
  • 標準的なキーボードで置いてある場所をイメージして置いたつもりだがまだちょっと覚えきれておらず探してしまうことが多い。

その他の設定

  • Auto mouse layerは有効なままで使用している。
    • keyball44では暴発が多くてOFFにしていたが、cocot38miniだと意図せずトラックボールに触れてしまうことは少なく、あまり困っていない。
  • Tap Holdの設定
    • Ignore Mod Tap InterruptのみONにしている。
      • Mod Tap Keyと通常キーを高速で押下した場合に両方をTapとして扱う設定。
      • これをONにしておかないと「a」を含む文字列がまともに入力できない。

GoogleでSurfingkeysのgiが効かないことへの対応

ブラウザの操作のためにChrome拡張のSurfingkeys(GitHub - brookhong/Surfingkeys: Map your keys for web surfing, expand your browser with javascript and keyboard.)を入れているが、最近Googlegi(最初の入力欄にフォーカスするコマンド)が効かなくなって少々困っていたが、色々確認していたらi(入力欄にフォーカスする、画面内に複数ある場合は選択肢が表示される)なら効くということがわかったので、設定に次の記述を追加することで対応した。

// Googleでgiコマンドの動作を修正する
if (window.location.hostname === 'www.google.com' || window.location.hostname === 'www.google.co.jp') {
  api.map('gi', 'i');
}

それだけの話で大した修正でもないんだけど、こうすれば修正できるということはChatGPTと雑談していたら見つけた。1発で解決方法を提示してくれたわけではないものの、ダメだったと伝えたり手がかりっぽい情報を伝えたりするとすぐに他の案を出してくるので割とスムーズに解決策にたどり着けて体験が良かった。

Rで非等価結合 (2)

dplyrが1.1.0になった。これにより、Rで非等価結合 - もうカツ丼はいいよなで少し触れていたdplyrによる非等価結合が正式にサポートされた。

記事で取り上げていた例なら次のように簡潔な記述で結合できるようになった。

# データの準備
library(dplyr)
car <- tibble(
  car_model = c("CarA", "CarB", "CarC"),
  car_price = c(20000, 30000, 50000)
)
boat <- tibble(
  boat_model = c("Boat1", "Boat2", "Boat3"),
  boat_price = c(10000, 40000, 60000)
)

# car_price > boat_priceを条件とした非等価結合
car %>%
  left_join(
    boat,
    by = join_by(car_price > boat_price)
  )

## # A tibble: 4 × 4
##   car_model car_price boat_model boat_price
##   <chr>         <dbl> <chr>           <dbl>
## 1 CarA          20000 Boat1           10000
## 2 CarB          30000 Boat1           10000
## 3 CarC          50000 Boat1           10000
## 4 CarC          50000 Boat2           40000

また、上記のような単純な不等号を使った非等価結合以外にrolling join、overlap joinができるようになっている。これによっておそらく時系列データなどがかなり扱いやすくなるのではないかと思われる。

Rolling join

これは不等号を使った非等価結合であるが、結合の結果を最も「近い」1件に限定する(タイが存在すると1件になるとは限らないが、その場合の挙動は後述)。

例のデータならCarCに対応する部分の結合結果が1行に減る。

car %>% 
  left_join(
    boat,
    by = join_by(closest(car_price > boat_price)) # 条件をclosest()で囲む
  )

## # A tibble: 3 × 4
##   car_model car_price boat_model boat_price
##   <chr>         <dbl> <chr>           <dbl>
## 1 CarA          20000 Boat1           10000
## 2 CarB          30000 Boat1           10000
## 3 CarC          50000 Boat2           40000

Overlap join

結合条件に範囲を使うもの。連続値を離散値に変換するような場合に便利だろう。

price_class <- tibble(
  class = c("A", "B", "C"),
  lower = c(10000, 20000, 50000),
  upper = c(19999, 49999, 100000)
)
car %>% 
  left_join(
    price_class,
    by = join_by(between(car_price, lower, upper)) # 条件をbetween()で記述する
  )

## # A tibble: 3 × 5
##   car_model car_price class lower  upper
##   <chr>         <dbl> <chr> <dbl>  <dbl>
## 1 CarA          20000 B     20000  49999
## 2 CarB          30000 B     20000  49999
## 3 CarC          50000 C     50000 100000

Overlap joinは条件に指定した変数の値次第では複数の行にマッチしてしまい、行が増える可能性がある。それが意図しないものだった場合は困るので、複数行にマッチするケースが存在したらエラーを返すようにすることもできる。

price_class <- tibble(
  class = c("A", "B", "C"),
  lower = c(10000, 20000, 50000),
  upper = c(20000, 50000, 100000) # CarAとCarCが複数のclassに属する
)
car %>% 
  left_join(
    price_class,
    by = join_by(between(car_price, lower, upper)),
    multiple = "error" # 複数のマッチがある場合にエラーを返すようにする。
  )

## Error in `left_join()`:
## ! Each row in `x` must match at most 1 row in `y`.
## ℹ Row 1 of `x` matches multiple rows.

複数行マッチ時のwarning

等価結合の場合とrolling joinの場合、左辺のテーブルの特定の行に右辺のテーブルの複数行がマッチして結果の行が増えてしまうことがあり得るが、それは一般的に意図しないものであろう、ということでそういう場合に警告が出るようになった。

df1 <- tibble(id = c(1, 2, 3), x = c("a", "b", "c"))
df2 <- tibble(id = c(1, 1, 2, 3), y = c("w", "x", "y", "z"))
left_join(df1, df2, by = "id")

## Warning in left_join(df1, df2, by = "id"): Each row in `x` is expected to match at most 1 row in `y`.
## ℹ Row 1 of `x` matches multiple rows.
## ℹ If multiple matches are expected, set `multiple = "all"` to silence this
##   warning.

## # A tibble: 4 × 3
##      id x     y    
##   <dbl> <chr> <chr>
## 1     1 a     w    
## 2     1 a     x    
## 3     2 b     y    
## 4     3 c     z

ちょっと気を利かせすぎな気もするが、実際このようなケースは意図せず生じた場合に検出が難しく間違いにつながることがしばしばある。私も何度かハマった経験がある。なのでデフォルトで警告が出るくらいのが良いのだろう。

警告は先程も使用した引数multiple"all"を指定することでoffにできる。

left_join(df1, df2, by = "id", multiple = "all")

Cross join (交差結合)

他に結合関係の変更として、今まではleft_join(x, y, by = character())というあまり直感的ではない書き方をする必要があった交差結合がcross_join()によってできるようになった(by = character()を指定して交差結合を行うと、cross_join()を使うよう警告が表示される)。

cross_join(car, boat)

## # A tibble: 9 × 4
##   car_model car_price boat_model boat_price
##   <chr>         <dbl> <chr>           <dbl>
## 1 CarA          20000 Boat1           10000
## 2 CarA          20000 Boat2           40000
## 3 CarA          20000 Boat3           60000
## 4 CarB          30000 Boat1           10000
## 5 CarB          30000 Boat2           40000
## 6 CarB          30000 Boat3           60000
## 7 CarC          50000 Boat1           10000
## 8 CarC          50000 Boat2           40000
## 9 CarC          50000 Boat3           60000

その他

  • dplyr 1.1.0での変更点はおそらくこの記事dplyr 1.1.0 is coming soonが現状だとよくまとまっている。一般的にはsummarise().byによる一時的なグループ化ができるようになったことあたりが役立つのではないかと思う。
  • dbplyr 2.3.0においてはまだjoin_by()はサポートされていない。Pull request(Support `join_by()` by mgirlich · Pull Request #1074 · tidyverse/dbplyr · GitHub)が上がっているのでそのうち対応されるだろうと思う。備えよう。

Scrapboxで画像が多いページへの行リンクがずれないようにする

画像の高さが不定なので、画像の読み込みが遅れると最初の表示位置からスクロールがずれてしまうらしい。

色々試したけどURLがハッシュを含む場合にウィンドウの高さが変わったら(≒画像が遅れて読み込まれたら)アンカーを叩くという方法で対応している。

let lastClientHeight = 0;
document.addEventListener("scroll", () => {
  if(lastClientHeight !== document.body.clientHeight) {
    if(location.hash != "") window.location.href = location.hash;
    lastClientHeight = document.body.clientHeight;
  }
});

個人のプロジェクトの場合はUserScriptに書いているが、共同プロジェクトの場合はUserScriptを切っているのでChrome拡張に書いている。

参考

DTでNA、Infを扱う

RのDTパッケージを使うとJavaScriptのDataTablesライブラリ(DataTables | Table plug-in for jQuery)利用したインタラクティブなテーブルが作成できる。

しかし、JavaScriptのライブラリである都合上、Rでは可能なことが簡単には実現できないということもある。表題に挙げたNAInfの扱いもその一つ。

続きを読む

指数平滑移動平均における初期値の決定・調整方法とR、Pythonでの計算方法

指数平滑移動平均移動平均の一種で、数列のある値y_tに対応する移動平均S_tを、次のような漸化式で定める。


S_t = \alpha y_t + (1-\alpha)S_{t-1}

ここで\alphaは新しい値に対する重みの割合を調整する係数で、0 \lt \alpha \lt 1であり、1に近いほど直近の値の影響が大きくなる。

漸化式を級数展開して、次のような表現をすることもできる。


S_t = \alpha (y_t + (1-\alpha)y_{t-1} + (1 - \alpha)^{2} y_{t-2} + (1-\alpha)^{3} y_{t-3}  + ... )

y_tに近いほど重みが大きく、遠い値の重みが指数関数的に減衰していくことが分かる。

指数平滑移動平均はExponential Moving Averageの頭文字をとってEMAと呼ばれることが多いので、以下EMAとする。

続きを読む

Rで非等価結合

非等価結合 is 何

通常の結合(等価結合)においては、テーブル同士を結合する場合にそれぞれのテーブルから列を指定し、列の値が等しいという条件をもって行同士を結合する。

非等価結合は結合条件に不等式や範囲などの等値ではない条件を指定して結合する。非等結合、非等値結合、Non-equi Join、θ-Joinなどと呼ばれることもある。

Wikipediaのθ-Joinの説明(Relational algebra - Wikipedia)を例にする。

次のようなテーブルがあるとする。

car_model car_price
CarA 20000
CarB 30000
CarC 50000
boat_model boat_price
Boat1 10000
Boat2 40000
Boat3 60000

これらのテーブルから、car_price > boat_priceとなるような組合せを得るということを考える。それには、1つ目のテーブルに対して、car_price > boat_priceを条件とした結合を行えば良い。結果は次のようになる。

car_model car_price boat_model boat_price
CarA 20000 Boat1 10000
CarB 30000 Boat1 10000
CarC 50000 Boat1 10000
CarC 50000 Boat2 40000

このような結合は、SQLであれば結合条件にon car_price > boat_priceのような指定をすることで実現できる。ではRではどうすれば良いか?

続きを読む