餡子付゛録゛

ソフトウェア開発ツールの便利な使い方を紹介。

RでBEEP音を鳴らす方法

BEEP音を鳴らしたかったのですが、検索するとbeeprパッケージを使え、alarm関数を使えと言うコレじゃないソリューションや、system関数でOSのコマンドを叩けと言うベタな手が引っかかります。どうやらRでBEEP音を鳴らしたい人々はほとんどいないようです。

Cの標準関数でBEEP音がサポートされていないせいか、Rは標準ではBEEP音を鳴らせません。その他の音も鳴らせません。パッケージの力を借りる事になるのですが、audioパッケージのplay関数が正弦波を音に変えてくれるので、BEEP音の代わりに使えます。

早速、どれみふぁそらしど…とベタに鳴らしてみましょう。

library(audio)

# 12平均律の音階周波数を生成
mkHz <- function(h = 4){
    Hz <- 440*(2^(1/12))^{(-12*4):(12*h)}
    n <- length(Hz)
    scale <- c("C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B")
    j <- 1
    lst <- list()
    while(0 < n){
        lst[[j]] <- paste(scale[1:min(n, 12)], j, sep="")
        j <- j + 1
        n <- n - 12
    }
    names(Hz) <- unlist(lst)
    Hz
}

# 音階と音程と音量から正弦波を生成
mkWave <- function(..., length, volume, rate){
    if(0 >= ...length()) stop("no scale inputed!")
    Hz <- mkHz()
    lst <- list()
    for(i in 1:length(...elt(1))){
        sinc <- 0
        for(j in 1:...length()){
            s <- ...elt(j)[i]
            if(!is.na(Hz[s])){
                sinc <- sinc + sin(1:rate/rate*(2*pi)*Hz[s])
            }
        }
        sinc <- sinc / ...length()
        lst[[i]] <- (sinc * volume[i])[1:(length[i])]
    }
    unlist(lst)
}

rate <- 44100 # サンプリングレート
scale <- c("C5", "D5", "E5", "F5", "G5", "A5", "B5", "C6")
length <- rep(rate/4, length(scale))
volume <- rep(1/4, length(scale))

# playで音をならし、waitで鳴らし終わるのを待つ
wait(play(mkWave(scale, length = length, volume = volume, rate = rate), rate))

正弦波を加算すれば和音(コード)になりますし、原理的には色々とできるわけですが、MIDIの再発明みたいな事になるのでこの辺でやめておこうかと思っていたのですが、正解/不正解/挨拶の効果音を用意しました。

correct <- function(rate = 44100){
    scale <- c("E5", "C5", "E5", "C5", "E5", "C5")
    length <- c(rep(rate/8, length(scale)-1), rate/4)
    volume <- rep(1/4, length(scale))
    wait(play(mkWave(scale, length = length, volume = volume, rate = rate), rate))
}

wrong <- function(rate = 44100){
    scale1 <- c("F#3", "F#3")
    scale2 <- c("G2", "G2")
    length <- c(rate/3, rate/1.5)
    volume <- rep(2/3, length(scale1))
    wait(play(mkWave(scale1, scale2, length = length, volume = volume, rate = rate), rate))
}

bow <- function(rate = 44100){
    length <- c(rate, rate, rate)
    volume <- rep(2/3, 3)
    wait(play(mkWave(
        c("C3", "G2", "C2"),
        c("C4", "G3", "C3"),
        c("E4", "D4", "E4"),
        c("G4", "F4", "G4"),
        c("C5", "G4", "C5"),
        c("", "B3", ""),
        length = length, volume = volume, rate = rate), rate))
}

fanfare <- function(rate = 44100){
    scale <- c("F4", "A4", "D5", "F5", "", "D5", "F5")
    length <- c(rep(1/5, 6), 1/2)*rate
    volume <- rep(1/2, length(scale))
    wait(play(mkWave(scale, length = length, volume = volume, rate = rate), rate))
}

bigben <- function(rate = 44100){
    scale <- c("F4", "A4", "G4", "C4", "F4", "G4", "A4", "F4", "A4", "F4", "G4", "C4", "C4", "G4", "A4", "F4")
    length <- rep(c(1, 1, 1, 2), 4) * rate
    volume <- rep(1/2, length(scale))
    wait(play(mkWave(scale, length = length, volume = volume, rate = rate), rate))
}

bow()
fanfare()
correct()
wrong()
bigben()

ggplot2やplotlyを使わないbubble plot

Rではggplot2やplotlyを使うと簡単にbubble plotを描画できますが、標準のplotを使ってもそんなに手間暇でもないです。より楽ができて、見栄えのするggplot2かplotlyの利用をお勧めしますが。

1. データセット

以下のデータセットをプロットすることを考えましょう。bubble plotを使う用途では滅多に無いと思いますが、負の数も入れておきます。

set.seed(26)
n <- 30
x <- runif(n)
y <- runif(n)
z <- seq(-1, 1, length.out=n)

2. プロット関数の引数を計算する関数

plotにつけるパラメーターcexとbgに渡すベクトルを計算する関数を用意します。JavaScript風のクラスになっている気がしますが、気にしないでください。

pch_properties <- function(z, zlim = c(min(abs(z)), max(abs(z))), 
    colors = c(rgb(1.0, 0, 0, 0.5), rgb(0, 0, 1.0, 0.5)), 
    cex.min = 1, cex.max = 10){

    A <-  sqrt(zlim[2])/cex.max

    list(
        getCex = function(...){
            a <- list(...)
            if(0 < length(a) && is.numeric(a[[1]])) z <- unlist(a) 
            browser(expr = !is.numeric(z))
            r <- numeric(length(z))
            r[z!=0] <- sqrt(abs(z[z!=0]))/A
            r[r<cex.min] <- cex.min
            r
        },
        getBg = function(...){
            a <- list(...)
            if(0 < length(a) && is.numeric(a[[1]])) z <- unlist(a) 
            colors[1 + (z>=0)*1]
        }
    )
}

3. 凡例のパラメーターを調整する関数

凡例のパラメーターを調整して、凡例を表示する関数を用意します。

writeLegendtoRight <- function(...){
    # 凡例のパラメーターに凡例非表示を追加する
    params <- c(0, 0, list(...))
    params[["xpd"]] <- TRUE
    params[["plot"]] <- FALSE
    params[["xjust"]] <- 0

    # legendのサイズ計算の補正用の値
    line <- 1
    if(is.numeric(params[["line"]])){
        line <- params[["line"]]
        params[["line"]] <- NULL
    }

    # 描画領域のサイズを計算
    r_legend <- do.call(legend, params)

    # 表示位置を計算(描画エリア外にセット/xpd=TRUE)
    usr <- par()$usr
    params[[1]] <- usr[2] + line*strheight("h")
    params[[2]] <- usr[4]

    # 非表示オプションを消す
    params[["plot"]] <- NULL

    # 描画する
    do.call(legend, params)
}

4. プロット

準備が済んだのでプロットしましょう。

# 変数zからプロットのパラメーターを計算
pp <- pch_properties(z)

# 描画領域外に凡例を描くので、右側の余白を大きくとる
par(mar = c(4, 4, 1, 10))

# プロットを行なう
plot(x, y, pch = 21, cex = pp$getCex(), bg = pp$getBg())

# 凡例に使うzの値
i <- c(-1, -0.5, -0.25)
i <- c(i, 0, -1*rev(i))

# 凡例を描く
writeLegendtoRight(0, 0, 
    legend = sapply(i, function(i){
        substitute(z==a, list(a = round(i, 2)))
    }),
    x.intersp = max(pp$getCex(i))/2 - 2,
    y.intersp = max(pp$getCex(i))/2 - 1,
    adj = c(0, 0.5), 
    pch = 21,
    col = "black",
    pt.cex = pp$getCex(i),
    pt.bg = pp$getBg(i), 
    bty = "n",
# 表示位置調整 
    line = 2)


5. まとめ

pointsの代わりにboxplotを置けたりする用途が謎のプロット関数symbolsを使ってもよいのですが、どちらにしろ凡例の表示が同様に忙しくなります。

DiagrammeR/Mermaid.jsで描いた図をreveal.jsに何とか埋め込む方法

Rで簡単にフローチャートやシーケンス図やER図を描いてくれるDiagrammeRパッケージで生成した図を、標準的な方法でR Markdown Format for reveal.js Presentationsで使おうとしたら、微妙に挙動不審なhtmlが出来上がったので、問題の回避策を記しておきたいとおもいます。

1. 標準的な方法とその問題

.Rmdのマークダウン記法の部分のRのコードチャンクに

DiagrammeR::mermaid("graph LR; d3.js-->Mermaid.js; d3.js-->Graphviz; Mermaid.js-->DiagrammeR; Graphviz-->DiagrammeR;")

と描くのが最も簡潔な方法ですが、適切に表示されたりされなかったり*1、他のスライドのCSSの設定が変化してしまったり、挙動不審になります。
解決方法がないか検索してみたのですが、どうもreveal.jsとMermaid.jsがその構造から相性が悪いようなので諦める事にします。

2. SVGに吐き出す方法とその問題

DiagrammeR::mermaidは、htmlwidgetオブジェクトを生成します。DiagrammeR::grVizもhtmlwidgetオブジェクトを生成し、そのhtmlwidgetオブジェクトはSVGに変換して保存することができます*2。ならば、同様に、DiagrammeR::mermaidが生成したhtmlwidgetオブジェクトもSVGに変換したいところですが、Rが異常終了するので無理そうでした。

library(DiagrammeR)
library(DiagrammeRsvg)
m <- mermaid("graph LR; d3.js-->Mermaid.js; d3.js-->Graphviz; Mermaid.js-->DiagrammeR; Graphviz-->DiagrammeR;")
svg <- export_svg(m) # Rごと異常終了する
con <- file("m.svg", "w")
writeLines(m, con)
close(con)

html/javascriptの生成物がSVGに変換できるとは限らないわけで、DiagrammeRsvgのアップデートで改善される見込みも薄そうです。

3. htmlを生成してiframeで表示する

こういうわけで、htmlを生成してiframeで表示する方法を試します。

3.1. self_contained: false

R MarkdownYAMLヘッダーでself_contained: falseを指定します。

output:
    revealjs::revealjs_presentation:
        self_contained: false

プレゼン資料がファイル一枚でまとまらず、サブディレクトリも持っていく必要があるのが残念なところですが、どうもreveal.jsはiframeの扱いが上手くないので。

3.2. htmlの生成と保存

R Markdownのコードチャンクの中で

library(DiagrammeR)
library(htmlwidgets)

saveWidget <- function(...){
    args <- list(...)
    w <- args$widget
    sp <- w$sizingPolicy
    # 今後のhtmlwidgetsのアップデートでプロパティ名は変化する可能性あり
    sp$padding <- 0
    if(is.null(args[["width"]])) args$width <- 400
    if(is.null(args[["height"]])) args$height <- 300
    sp$defaultWidth <- args$width
    sp$defaultHeight <- args$height
    args["width"] <- args["height"] <- NULL
    w$sizingPolicy <- sp
    args$widget <- w
    do.call(htmlwidgets::saveWidget, args)
    con <- file(args[["file"]], "r", blocking=FALSE)
    lines <- readLines(con, encoding="UTF-8")
    targets <- grep("</body[^>]*>", lines)
    if(0<length(targets)){
        lines[targets[1]] = gsub("(</body[^>]*>)", 
            "<script>if(0>=document.body.clientWidth){ setTimeout(function(){ location.reload(); }, 1000) }</script>\\1", 
            lines[targets[1]])
    }
    close(con)
    con <- file(args[["file"]], "w", encoding="utf-8")
    writeLines(lines, con)
    close(con)
}

g <- mermaid("graph LR; d3.js-->mermaid; d3.js-->Graphviz; mermaid-->DiagrammeR; Graphviz-->DiagrammeR;")

saveWidget(widget=g, file="mermaid.html",
    background="transparent",
    selfcontained=FALSE,
    title = "example", 
    width = 400, height = 200) # defaultWidthとdefaultHeightの値は図のサイズに応じて変える

とhtmlを生成して保存します。
saveWidgetにフックして、パラメーターをすりかえつつ、生成したファイルにJavaScriptを追加しているわけですが、JavaScriptの追加理由が分かりづらいかも知れません。reveal.jsの中ではiframeのサイズが動的に変化するのですが、Mermaid.jsは読み込み時に1回しか描画しないため、サイズ0で描画して終わりになる場合があります。そこでiframeのサイズが0の間は、1秒間隔でiframeをreloadするJavaScriptを追加しています。

3.3. iframeを書き込む

後はR Markdown

<iframe style="width:100%; height:70vh; transform:scale(1.732051); transform-origin:0 0;" src="mermaid.html"></iframe>

とiframeで作成したファイルを呼び出すように書いておけば、表示を一体化できます。

なお、iframeの内側は外側よりキャッシュが強くリロードしても表示が変わらないときがあるので、作業中に困ったら、CTRL+Rを押すか、ブラウザーを一旦終了してファイルを読み直しましょう。

4. まとめ

気づくとjQueryやVue.jsに限らず無数にあるJavaScriptフレームワークですが、諸般の理由で同時利用が困難なことがあります*3。何とか両方使いたいときは、iframeで片方を隔離してしまうのが無難です。

*1:Mermaid.jsの図があるページでリロードしないと表示しないように思えました。以下のようになります。

*2: DiagrammeR入門 画像ファイルなどへの出力 - Qiita

*3:R Markdownの発展版であるQuartoでも、コードチャンクの実行後にMermaidで描いた図が表示されないと言う問題が2022年9月28日時点である(Quarto Revealjs - Mermaid diagrams don't show after R code chunks, pictures - R Markdown - RStudio Community)。

Rでバイナリファイルを置換

なるべく避けた方が良い気がするのですが、Rでもバイナリのデータを扱えます。0から255の範囲を整数のpack形式と、0か1のunpack形式のraw型として処理されるのですが、C言語あたりから入った人だとややこしく感じるかも知れません*1

1. pack/unpack形式のraw型

やろうと思ったら画像ファイルのEXIFの情報を引っ張っていて、それを統計処理にかけたりできます。

  • よくあるコード番号をraw型にする場合は、例えばASCIIコード32をintToBits(32)とするとunpack形式のraw型になります。さらにpackBits(intToBits(32))とするとpack形式のraw型になります。
  • pack形式のraw型は文字列として取り扱うためにcharacter型にでき、例えばrawToChar(packBits(intToBits(32)))とすると空白が得られます。rawToCharの逆の操作になる関数もあって、charToRaw(" ")で文字列をraw型にでき、16進数の文字コードが表示されます。文字コード(数値文字参照)が連なった文字列をまとめてraw型に変える場合は、やや煩雑で、例えば`s <- "737472696e67"; s_raw <- as.raw(as.hexmode( sapply(seq(1, nchar(s), 2), function(i){ substr(s, i, i + 1) }) ));などとします*2
  • バイナリ形式のI/OはreadBin関数とwriteBin関数を用いますが、pack形式のraw型でデータを読み込み、また書き出すことになります。画像ファイルのEXIF情報などを読んだりするのに使えます。

2. バイナリ置換

RからはCを呼べるので、あまりRでバイナリ処理はしないだろうなと思っていたら、バイナリ置換をすることになりました*3

binreplace <- function(input, output, pattern, replacement, maximum=1, buffsize=1024*1024){

    istream <- file(input, "rb", blocking = FALSE)
    ostream <- file(output, "wb")

    if(length(pattern)<buffsize) buffsize <- length(pattern)*10
    buf <- readBin(istream, "raw", buffsize) 

    i <- 1
    counter <- as.integer(maximum) # maximum回だけ置換するが、負の数を入れれば無限回
    EOF <- FALSE
    while(!EOF){
        while(i <= length(buf) - length(pattern) + 1){
            # RでKnuth Morris Pratt法を書く元気がないのでバカ検索
            if(0!=counter && 
                all(buf[i:(i + length(pattern) - 1)] == pattern)){
                writeBin(replacement, ostream)
                i <- i + length(pattern)
                counter <- counter - 1
            } else {
                writeBin(buf[i], ostream)
                i <- i + 1
            }
        }
        abuf <- readBin(istream, "raw", buffsize)
        if(i<=length(buf)) buf <- c(buf[i:length(buf)], abuf)
        else buf <- abuf
        i <- 1
        if(0==length(abuf) && !isIncomplete(istream)) EOF = TRUE
    }

    writeBin(buf, ostream)

    close(ostream)
    close(istream)
}

binreplace("input.txt", "ouput.txt", charToRaw("old phrase"), charToRaw("new phrase"), 1)

わざわざblocking = FALSEにして、ファイル末尾に達する前に0バイト入力の可能性をつくって、isIncompleteでそうなっていないか確認しています。
なお、16進数の文字列に変えて正規表現で置換したらどうかと思って試してみたのですが、文字列からraw型に戻すのが手間なためか、逆に3倍ぐらい時間がかかるようになりファイルサイズの上限がつくものの処理時間を半分ぐらいに短縮できました*4

3. まとめ

明らかに遅いので使えるならばsedあたりを使いましょう。パッケージを追加しないとflockもできませんし。

*1:pack/unpackでアセンブラを思い出しますね。

*2: s_rawを表示させると16進数のベクトルに見えますが、rawToChar(s_raw)で文字列に変換されるのでraw型になっているのが分かります。

*3: localeがJapanese_Japan.932のRはUnicodeの絵文字(e.g. ✓ ♨ ♡ ⚑)を文字として認識できず、テキストファイルとして無理に読み込むと絵文字を消してしまいます。あるファイルの中のある単語を一つ置換したかったのですが、テキスト処理では不可能に・・・と思っていたんですが、2週間経って`Sys.setlocale("LC_ALL", "C")`して処理してから`Sys.setlocale("LC_ALL", "C")`で間に合うことに気づきました。警告は出ますが。なお、system関数でsedを呼んだら済むのですが、sedが入っていないWindows環境も考えたかったので手間暇かけています。

*4: コードを消すかと思っときに、文字列からraw型に戻すのにforしているところをsapplyに代えたら速くなるかなと思ったら、速くなりました。ただし処理対象のファイルサイズが指定するバッファサイズ以下という制限がつくので、今回は遅い方のコードのままにします。

Rによる解析結果をShinyでインタラクティブにプレゼンテーションしよう

数理モデルの数値解析や統計解析では、パラメーターを色々といじったり、変数や分析期間を変えたりして計算をやり直したり、結果の一部分を切り取って参照したいときがあります。プログラムをぽちぽちと変えていけば実現できるわけですが、ぱっと見ではそのコードを書いている人以外は何をしているのか理解できないので、それは説明には向いていないやり方です。

文章に結果をまとめる都合もあり、あらかじめ多種多様な分析結果を用意しておくことが多いと思いますが、GUIのフォームに入力したパラメーターに応じてリアルタイムに表示を変えて見せるのも有効な手段です。RはGUIアプリケーションの構築に向かないツールですが、RStudio社が出しているShinyパッケージで手早く入力フォームの構築と処理を書くことができます*1

1. Shinyの使い方

やり方を説明しようと思ったのですが、公式ページからたどれるドキュメントが十分によく整備されているので説明すべきことが残されていません。英語が苦手な人でも、

install.packages("shiny")

をした後に、

library(shiny)
# CTRL+CかSTOPボタンで中断
runExample("01_hello")

と例を表示させると、そこに例のコードも表示されているので、何となく使い方が分かることでしょう。なお、例はインストールされたshinyパッケージのフォルダーの中のexamplesフォルダーに格納されていて、01_hello, 02_text, 03_reactivity, 04_mpg, 05_sliders, 06_tabsets, 07_widgets, 08_html, 09_upload, 10_download, 11_timerの11種類があります。

2. 実例

例も豊富なので無用感もあるのですが「Rでエッジワース・ボックスを描こう」で使ったコードを関数化して、Shinyを通してプロットする(ローカルで動かす)ウェブ・アプリケーションを作成してみました。ソースコードの全体はMercurialのリポジトリにして公開しているので、そちらを参照してください。

そこそこそれっぽく動くと思いますが、Shinyのための作業は

  • コード例01_helloにスライドバーとチェックボックスを追加
  • グラフィックス・デバイスの指定部分を含まないプロット部分を、スライドバーやチェックボックスからの入力に対応した引数を持つ関数化
  • コントロール(フォーム)に変更があった場合に呼ばれる関数serverの中のrenderPlotの中のhist関数を、前段の作業でつくったdrawEdgeworthBox関数に置換

したぐらいです。統計解析とちがって見栄えが全てだけに、習うより慣れろ系のツールですね。

3. RADとして使う分には便利

迅速開発ツール(RAD)なので、楽をする範囲では*2レイアウトに制約が大きく、独自仕様のコントロールをつくったりはできないですが、数値解析のパラメーターを変えて見るのには十分な機能だと思います。
なお、スライドバーやチェックボックスなどの配置するコントロールを規程する関数の引数themeにBootstrap theme objectを与えると、色調や利用フォントを変えられます。例えば、

ui <- fluidPage(
    theme = bslib::bs_theme(bootswatch = "minty"),

というようにすると、テーマmintyが適応されて全体の色合いが調整されます。利用できるパッケージ内蔵テーマは

bootswatch_themes()

で一覧することができ、内蔵していないのは、

ui <- fluidPage(
    theme = "path/to/bootstrap.css",

というような感じで、ファイル参照して利用することができるようです。
さらに、入力フォームの定義にも結果の出力にもwithMathJax関数でTeX記法が使えるので、見栄えはそこそこの説明資料としては十分に機能するはずです。

4. C拡張やFortran呼び出しとあわせると効果が高い

特に微分方程式モデルの説明に凄くよさそうなのですが*3、応答までの時間は数秒ぐらいにしておかないと、見ている人はストレスを感じます。ところが微分方程式モデルはその解法から繰り返し計算の山なので、Rでの処理に向きません。数理モデル次第ですが、CやFortranと比較して30倍ぐらい遅くなります。逆に言うとFortranあたりで書いた数値解析をRから呼び出し*4、Shinyでインタラクティブにプレゼンテーションするのは、高速性と使い勝手の相乗効果が出て良さそうです。

*1:外部プログラムからRをコントロールする術は色々とあるのですが、本格的にウィンドウ・アプリケーションを書ける言語は煩雑なことが多いです。

*2: htmlを埋め込むこともできるので、頑張ったら色々できると思います。

*3:連続的に変化するパラメーターの無い計量分析結果の説明は、何かの方法で提示するモデルを絞って、静的なプロットを何枚か用意する方針の方が分かりやすい。

*4: Fortran and R – Speed Things Up | R-bloggers

Rでnames(x) <- "a name"的な代入を定義するには

今まで一度たりとも作る必要を感じたことがなく、言語定義を見ても特段何も書いていないようなので、関数呼び出しへの代入(?)はユーザー定義はでき無いのかなと思っていた*1のですが、ヘルプを見ていたら*2

The functions 'dim' and 'dim<-' are internal generic primitive functions.

などと書いてあって、dimとは別にdim<-と言う関数が別にあるように書かれています。実際、get("colnames")とget("colnames<-")をすると、二つで中身は異なります。
つまり、名前の末尾が<-にすれば関数呼び出しへの代入定義になるのかなと思って、

assign("example<-", function(x, value){
	2*value - x
})

と書いたら、

x <- 7
example(x) <- 3

とできました。xの値が-1になります。同時に、

4 -> example(x)

もできるようになります。ユーザー定義のS3/S4オブジェクトへの値の代入などに便利そうですが、見かけたことが無いような、覚えていないだけなような・・・

*1:か、読んだ説明を忘却していた

*2:なんでわざわざ調べているのかと言うと、プログラミング言語比較コラムで、RにできてJuliaにできないことの一つとして書いてあったので、ふと思い立ちました。

Rの関数の引数が参照渡しから値渡しに変わるとき

実用上はRの関数の引数は値渡しと理解しておけばよいのですが、引数を他の変数に代入したり、関数の中の関数呼び出しで使ったりする前は、参照渡しになっています。実際、

fn1 <- function(x) x^2
fn2 <- function(x, fn){
    rm("fn1", envir = parent.env(environment()))
    if(!exists("fn1", envir = parent.env(environment()))) print("fn1がありません")
    fn(x)
}
print(fn2(3, fn1))

と言う風に、計算に使う前に親環境のオブジェクトを消してしまうと、「fn2(3, fn1) でエラー: オブジェクト 'fn1' がありません」とエラーが出ますが、

fn1 <- function(x) x^2
fn2 <- function(x, fn){
    fn0 <- fn
    rm(fn0) # fn0は用済み
    rm("fn1", envir = parent.env(environment()))
    if(!exists("fn1", envir = parent.env(environment()))) print("fn1がありません")
    fn(x)
}
print(fn2(3, fn1))

他の変数に代入すると、親環境のオブジェクトを消してもエラーなく実行できます。実用上、このようなコードを書く必要は無いので、一生、気づかない人は多そうですが。