rch850 の上澄み

技術的な話題とか、雑談とか。タイトルを上澄みに変えました @ 2020/09/02

放送大学の「統計的因果推論の考え方と技術」を見た

BS で、放送大学「統計的因果推論の考え方と技術」の2回目以降を見たら、本当に何もわからない状態から、ある程度、用語や考え方がわかるようになった気がします。

1月31日(日)の 12:45 から 18:45 にかけて一挙放送があるので、ちょっとでも興味がある方はぜひ録画しておきましょう!

なお、講師は書籍「統計的因果推論」の著者の岩崎先生です。

www.amazon.co.jp

視聴の経緯

ここ1年ぐらい、因果推論が気になるけど勉強してなかったところ、Twitter でたまたま奥村先生のこのツイートを見かけたので、見てみることにしました。

このツイートを見たのはとても運が良かったです。気づくのが若干遅れて、2日目からの視聴でした。

なお、自分自身の因果推論パワーはというと、さっき書いた書籍「統計的因果推論」は持ってないけど、効果検証入門Kindle で買って、最初の少しだけ読んだ状態でした。この放送で出てきますが、「独立の記号は ⊥」と聞いて「そういえばそうだった、きがする」程度です。ベイズは少しかじってたので、条件付き独立の書き方は知ってました。

全体的な感想

数式、図、実例、口頭での説明のバランスが良かったのか、個人的にはスルスルと頭に入ってきました。とだけ書くと簡単そうですが、簡単ってわけではないです。予備知識がないせいか、個人的には、メモを諦めて視聴に集中すればなんとかついていけるペースでした。まずは視聴に集中して、気になるところは録画で一時停止しながらメモを取っていました。

内容については、細かく書くとキリがないので、考え方として理解できてよかったなと思う3つを書いておきます。

  • A さんが広告を見たら買う/買わない Y_i(1)。A さんが広告を見なくても買う/買わない Y_i(0) を、潜在的アウトカムアプローチ \{Y_i(1), Y_i(0)\} という考え方を使って表現する。これを活用して本来測れない数字を推定したりする。
  • 「広告を見た A さん、B さんが買う確率は50%」という考え方ではなく「A さんは広告を見たら買う」「B さんは広告を見ても買わない」というのは決まっていて、確率的なのはサンプルとして A さんが選ばれるか B さんが選ばれるかだという考え方をする。
  • 実験するなら、年齢、性別、居住地などが偏らないように(共変量が同じ個体を処置、対照で均等にする)。共変量がたくさんあって、完全一致する人なんていないよ、というときは、傾向スコアという手段を使うこともできる。

こんな考え方をベースにしながら、いろいろな手法を紹介していく内容でした。最終日は「薬を飲みなさいね、といった人が飲まなかったとき、それが効果の推定にどう影響するか」なんてことまで気にした話が出てきます。

本を読むよりテレビで見たほうが頭に入ってくるかも、という方はぜひ一挙放送を見てみてください。

シェルを spacefish から starship へ

久々に開発環境保守。

Catalina で zsh がデフォルトになってから1年が経って、fish を離れてデフォルトの zsh で生活しようかという気持ちが少しだけ芽生えた。

  • なるべく環境に対して複雑な設定をしたくない。
  • fish は複雑な設定をせずにいい感じにしてくれるけど、そもそも fish を入れる時点で環境に対してだいぶ手を入れている感じがある。
  • zsh で fish ぐらいの便利さを手にしようとすると、結構手を入れなきゃいけない気がする。試してないけど。
  • じゃぁやっぱり fish でいいや。

ということで fish 路線を続行。

fish では spacefish を使っていた。過去ログさかのぼったら、意外とそんなに昔じゃなくて、去年の3月だった。最近どうなってるかなと見に行ってみたら……

f:id:rch850:20201114023320p:plain

どうやら Starship というものが後継となったそうで。

Starship を試すために、まずはいったん spacefish を削除。これまでありがとう。*1

fisher rm matchai/spacefish

Starship のガイドを見ると、まず Prerequisites として Nerd Font を入れましょうとある。前々から ligature が気になっていた FiraCode の Nerd Font 版をインストール。インストールには Homebrew を使いました(手順)。

brew tap homebrew/cask-fonts
brew cask install font-fira-code-nerd-font

そして Starship 自体もインストール。

brew install starship

いったん starship init fish | source してそれっぽく動いたので config.fish に設定を追加。これにて完了!

……だったんですが、メールアドレスが出るのがなんとも言えない気持ち。

f:id:rch850:20201114024406p:plain

そもそもこのメールアドレス何?ってことで、いろいろ調べてみた結果、GCP の設定ファイル .config/gcloud/configurations/config_default に書いてあるメールアドレスをいじったら変わったので、どうやら GCP の設定を表示しているようです。

スクショを撮った時にふせなきゃいけないのはちょっと面倒だな、と思ったので、対処方法を調べました。

あまり難しい話ではなく、設定方法にある通り ~/.config/starship.toml を編集すればメールアドレスが出なくなりました。めでたしめでたし。

*1:fish のプラグイン管理には fisher を使ってます。

コワーキングスペースの申請のため Groovy x POI で Word ファイルを操作する

ふくもく会では、福井産業情報センターのコワーキングスペースにお世話になることが多いのだけど、ここを使うには利用申請書を提出する必要がある。この利用申請書が .doc ファイルで、これをなんとかしてサクッと記入できないか考えてみた。

最初は勉強のために Rust で書こうとしたけど、Rust には docx を編集するライブラリ、その名も docx しかなさそうだった。じゃぁ Python でいいかと調べてみても、こちらも docx を編集するライブラリしかなさそう。例えば python-docx

諦めて手書きするかなーと @macoshita にぼやいてたら「この分野は POI が最強っすね」と言われて Apache POI のことを思い出した。Java かー。もうちょっと軽い気持ちで書きたいんだよなー。ということで Groovy から使ってみることにした。

Groovy 書くのが5億年ぶりなので、イチから調べてみたところ、Grape という仕組みがあって、依存をすっと書けるらしい。サンプルとして次のコードが書かれていた。

@Grab('org.springframework:spring-orm:3.2.5.RELEASE')
import org.springframework.jdbc.core.JdbcTemplate

build.gradle とかが無くても、こんな感じで groovy ファイルに @Grab で書いとくだけで、勝手に依存する jar を取ってきてくれるんだって。夢か?Denoか?

そんなわけで、これを活用して POI でコワーキングスペースの利用申請書の doc ファイルを開いて、適当にテキスト置換して、書き出すコードを書いてみた。

// TextRangeReplatement test as an HWPFDocument example
// http://svn.apache.org/viewvc/poi/trunk/src/scratchpad/testcases/org/apache/poi/hwpf/usermodel/TestRangeReplacement.java?revision=1872041&view=markup

@Grab('org.apache.poi:poi:4.1.2')
@Grab('org.apache.poi:poi-scratchpad:4.1.2')
import org.apache.poi.hwpf.HWPFDocument
import org.apache.poi.hwpf.usermodel.Range
import org.apache.poi.hwpf.usermodel.Section

FILENAME = 'coworking_format1.doc'

try (HWPFDocument daDoc = openSampleFile(FILENAME)) {
    Range range = daDoc.getRange()

    def 申請日 = '令和2年 11月 5日'
    def 住所 = '福井県福井市'
    def 利用日 = '令和2年 11月 23日(月)'
    def 氏名 = '福井太郎'

    // dump(range)
    range.replaceText('平成   年   月   日(  )', 利用日)
    range.replaceText('年   月   日', 申請日)
    range.replaceText('住所   ', '住所 ' + 住所)
    range.replaceText('氏名   ', '氏名 ' + 氏名)

    dump(range)
    daDoc.write(new File('coworking.doc'))
}

void dump(Range range) {
    println("numParagraphs: " + range.numParagraphs())
    for (int j = 0; j < range.numParagraphs(); j++) {
        println(j + '[' + range.getParagraph(j).text() + ']')
    }
}

HWPFDocument openSampleFile(String filename) {
    try {
        InputStream is = new FileInputStream(filename)
        try {
            return new HWPFDocument(is)
        } catch (Throwable e) {
            is.close()
            throw e
        }
    } catch (IOException e) {
        throw new RuntimeException(e)
    }
}

これを groovy main.groovy とかで実行するだけ。

f:id:rch850:20201105010826p:plain

おー、できたできた。便利すぎるな。この方向で進めてみようと思う。

細かいメモ:

  • POI の数ある機能のなかで Word ファイル (.doc) を操作する機能は HWPF という部分。
  • Word ファイルを読むと HWPFDocument という形になる。
  • コメントに書いた通り TextRangeReplacement というテストコードが HWPFDocument でテキスト置換するときのいい例になっている。実際、このコードからすべてを学んだと言ってもいいぐらい。
  • 「住所」を「住所 福井県」に Range#replaceText すると処理が終わらない。たぶん無限ループにハマってる。仕方なく「住所   」を「住所 福井県」に置換して無限ループを回避した。他にも Section を特定して Range#insertAfter を駆使するなど、細かい工夫が要りそう。

Angular の ui-router で lazy loading を書くと a の href がイマイチ

  • Angular の ui-router を使っていて
  • uiSref を使ってリンクを書いていて
  • lazy loading を素直に実装すると
  • lazy loading 後のページを指す A 要素の href がイマイチになる

イマイチ、というのは、だいたいこんな雰囲気のルーティングをしたときに……

// app.module.ts
{
  state: 'items.**',
  url: '/items',
  loadChildren: () => import('./items/items.module').then(m => m.ItemsModule)
}

// items/items.module.ts
{
  state: 'items',
  url: '/items',
  component: ItemsComponent
}, {
  state: 'items.new',
  url: '/new',
  component: ItemsNewComponent
}

lazy loading 前のページから <a uiSref="items.new">New Item</a> すると、その a 要素の href が /items/new になってほしいところ /items になってしまうというものです。

クリックして遷移するだけなら動作に問題ないのですが、右クリックして別タブで開くといった操作をすると /items が開いてしまって残念な感じです。

items.edit という状態の URL は /items/new ですよ」という情報が lazy loading 後じゃないと得られないので、当然といえば当然ですね。

これに対して解決策も示されていますが、なかなか煩雑な印象です。

github.com

Angular 標準のルーターなら最初から routerLink で URL を直接書くので、特に問題になりません。

ただの日記 2020/10/19

前回更新から1ヶ月空いちゃって、このままだと文章化スキルが落ちそうなので、ただの日記で文章化スキルをメンテします。

ISUCON 10 本選どうだったのか

前回更新「ISUCON 10 予選を nodejs 実装で突破しました(へしこず) 」で書いた話の流れですね。さる10月3日に行われた ISUCON 10 本選。結果は暫定で最下位、最終的に最下位から2位でした。肌感覚としても、歯が立たなかった、というもので、しばらく打ちひしがれていました。

ありがたいことに、いくつか賞をいただきました。

ひとつは「またあいま賞」。fail せず点数がついたチームの中で、下位のチームに与えられた賞です。がんばろう。

もうひとつは「ブログアワード次点」。これはアフターイベントの中で発表された賞でした。予選突破の記事でベンチマークシナリオを解析していたところが評価されていました。あと nodejs 実装で挑んだことも。ブログ書いてよかったー。ありがとうございます!

f:id:rch850:20201019232747p:plain

youtu.be

ワークマン本を読んだ

こちらのところてんさんのツイートを見て気になっていたワークマン本を読みました。

会社で数ヶ月前から毎週統計の勉強会をやっているところで、グサッと来る内容でした。需要予測のアルゴリズムの検証会議を週1で開催して、条件を見直しているあたりとか。こういう本はめったに買わないんですが面白かったです。

統計といえばこんな本も買いましたね。正直、最初のページの「This is a ball」が一番の謎。

PG BATTLE 2020 に出ます

ISUCON で魂が抜けているところですが PG BATTLE 2020 に出てきます。チームメンバーの実力差がそこまで大きくないので、どの問題をやるかはくじ引きで決めました。魂をつなぎとめたい。

以上!

ISUCON 10 予選を nodejs 実装で突破しました(へしこず)

「へしこず」の rch850 です。ISUCON 10 の予選を nodejs 実装で通過しました。奇跡的な予選通過の ISUCON 5 以来、2度目の本選進出です。やったぜ!チームメイトは machosita と emittam です。いつもありがとうございます。

個人的には nodejs で予選を突破できたのがうれしいです。 nodejs への思いは最後に。

macoshita の予選レポートが実況風味なので、こちらは箇条書きにします。実況風味はまた時間があるときに。

なおリポジトリはこちらです。

github.com

スコア推移はこう。

やったこと

サーバ構成

シンプルにこうしました。DB 分割したチームが結構いたようですが、そこまで考えてませんでした。しなくても勝てたし、して勝てたかは分かりません。

  • .101: ベンチマーカーのアクセス先。nginx, API サーバ, redis
  • .102: MySQL 専任
  • .103: API サーバ

効果が大きかったかもしれないもの

  • bot 対策 (nginx側, nodejs 側)
  • /api/estate/low_price と /api/chair/low_price をキャッシュ (commit)
    • どのベンチマーカーも最初にアクセスしてくる。そして更新頻度がとても低い
    • 物件は入稿時のみ、椅子は入稿時と在庫が切れた時にキャッシュを破棄
    • あとから Redis に乗せた (commit)
  • nazotte をワンクエリにして spatial index 追加 (commit)
    • 当初は latlon をレスポンスに含めていたけど、なんかベンチマークが不安定な感じがしたので念の為削除した (commit)

効果がそこまで大きくなかったかもしれないもの

  • /api/estate/req_doc/:id で id 29500 以下なら即 OK、それ以外は Redis に id をキャッシュしてその後のアクセスはキャッシュを見て OK (commit)
  • /estats/:id のキャッシュ (commit)
  • nazotte に limit 指定が無かったので追加 (commit)
    • これに気づいたのが残り10分切ってから。今からこれやる!?って10秒ほど相談して投入した。これが最後のコミット
  • rent, (stock, price) のインデックス追加 (commit)
  • (door_width, door_height), door_height, popularity のインデックス追加 (commit)
  • (door_width, door_height, rent), (kind, stock, price) のインデックス追加 (commit)
  • search 系のカウントと検索を Promise.all で並列化 (commit)
  • 不必要な SELECT * の改善
    • req_doc(資料請求)のクエリを念の為 SELECT 1 に (commit)
    • estate/search のクエリを念の為 SELECT id に (commit)

やろうとしたけどできなかったこと

自分がやってたとこ。悔しいので晒します。

  • recommended の OR を減らす
    • 短い2辺 (len1, len2 とする) が通ればいいから (door_width >= len1 AND door_height>= len2) OR (door_width >= len2 AND door_height>= len1) だけでいいよね?
    • const [len1, len2] = [chair.width, chair.height, chair.depth].sort() すればいいんだな。はいコミット (commit)
    • あれ?検証エラー?
    • ソート順間違えたかーー???えいやー const [_, len1, len2] = [chair.width, chair.height, chair.depth].sort() (パニック状態) (commit)
    • また検証エラー??????完全に正解してるでしょ????
    • MDN 見たら sort は「デフォルトではUnicodeコードポイントの昇順にソート」って書いてある!parseInt したら通るやろ! (commit)
      • [1, 10, 4].sort()[1, 4, 10] になると思い込んでたけど、実際は [1, 10, 4]
      • [1, 10, 4].sort((a, b) => a - b) なら [1, 4, 10] になる
    • 通らない……もう限界。revert します
    • ラスト数分でこうすればいいことに気づくが、もうコードを変える時間は残っておらず終了

      // ここまでのコード
      const [_, len1, len2] = [parseInt(chair.width), parseInt(chair.height), parseInt(chair.depth)].sort((a, b) => a - b);
      // 多分これが正解
      const [len1, len2] = [parseInt(chair.width), parseInt(chair.height), parseInt(chair.depth)].sort((a, b) => a - b);
      

どういう方向性で動いたか

今回はこれまでの ISUCON の動き方からちょっと変えてみました。

  • 落ち着いて取り組むようにした(というのは方針で抽象的なので、以降具体的な話)
    • 自信がある nodejs を選んだことで落ち着けたというのはあると思う
  • アクセスログからベンチマーカーの動きを見て、どうやって得点を稼げるか考えた(詳細は次のセクションに)
    • これは ISUCON 9 予選の反省からです
    • UA に UUID が無かったらやれてなかったかもしれない
    • これまでは slowlog (mysqldumpslow や pt-query-digest) や kataribe だけを見て、ボトルネックを解消していました。ボトルネックが得点に結びつくとは限らない、と考えて、今回は得点に関係しそうなボトルネックに注力しました
  • ウェブサービスを自分で触って挙動を理解するようにした
    • これも上と関係した話ですね
  • Redis を使うようにしたのが19時過ぎ(残り2時間弱)だったり、API サーバを増やしたのが20時過ぎ(残り1時間切ってる)だったりと、普段は早めにやっていた作業を、かなり後回しにした
    • 「早すぎる最適化」を懸念したり、ぬか喜びしたくないなという考えから、意図的に後回しにしました。結果にどう影響したかは分からないけど、たぶん良かったんでしょう

ベンチマーカーの行動パータン

nginx のアクセスログから調べました。User-Agent に UUID らしきものがついていたので、それで絞り込んでパターンを分類しました。

3パターンの行動が確認できました。他にもあったかもしれませんが、ひとまずこの3つで。末尾の数字はその時点での req_time です。これを見て、あるパターンのユーザーが得点に至るまでの合計時間を意識するようにしました。low_price は全パターンに効いてくるとか、search は回数が多いから大事とか。

ユーザーパターン 1(条件検索して資料請求)

"GET /api/estate/low_priced HTTP/1.1" 0.003
"GET /api/chair/low_priced HTTP/1.1" 0.071
"GET /api/estate/search/condition HTTP/1.1" 0.001
"GET /api/estate/search?page=0&perPage=25&rentRangeId=2 HTTP/1.1" 0.079
"GET /api/estate/search?page=3&perPage=25&rentRangeId=2 HTTP/1.1" 0.114
"GET /api/estate/search?page=3&perPage=25&rentRangeId=2 HTTP/1.1" 0.118
"GET /api/estate/search?doorWidthRangeId=1&page=0&perPage=25 HTTP/1.1" 0.168
"GET /api/estate/search?doorWidthRangeId=1&page=4&perPage=25 HTTP/1.1" 0.185
"GET /api/estate/search?doorWidthRangeId=1&page=3&perPage=25 HTTP/1.1" 0.180
"GET /api/estate/27131 HTTP/1.1" 0.002
"GET /api/estate/24611 HTTP/1.1" 0.001
"POST /api/estate/req_doc/24611 HTTP/1.1" 0.002

ユーザーパターン 2(なぞって資料請求)

"GET /api/estate/low_priced HTTP/1.1" 0.010
"GET /api/chair/low_priced HTTP/1.1" 0.176
"POST /api/estate/nazotte HTTP/1.1" 2.000
"GET /api/estate/low_priced HTTP/1.1" 0.016
"GET /api/chair/low_priced HTTP/1.1" 0.095
"POST /api/estate/nazotte HTTP/1.1" 0.223
"GET /api/estate/16302 HTTP/1.1" 0.005
"POST /api/estate/req_doc/16302 HTTP/1.1" 0.002

ユーザーパターン 3 (条件検索して購入)

"GET /api/estate/low_priced HTTP/1.1"
"GET /api/chair/low_priced HTTP/1.1"
"GET /api/chair/search/condition HTTP/1.1"
"GET /api/chair/search?depthRangeId=1&page=0&perPage=25 HTTP/1.1"
"GET /api/chair/search?depthRangeId=1&page=2&perPage=25 HTTP/1.1"
"GET /api/chair/search?depthRangeId=1&page=0&perPage=25 HTTP/1.1"
"GET /api/chair/search?color=%E3%83%8D%E3%82%A4%E3%83%93%E3%83%BC&page=0&perPage=25 HTTP/1.1"
"GET /api/chair/search?color=%E3%83%8D%E3%82%A4%E3%83%93%E3%83%BC&page=3&perPage=25 HTTP/1.1"
"GET /api/chair/search?color=%E3%83%8D%E3%82%A4%E3%83%93%E3%83%BC&page=2&perPage=25 HTTP/1.1"
"GET /api/chair/2032 HTTP/1.1"
"GET /api/recommended_estate/2032 HTTP/1.1"
"GET /api/chair/19330 HTTP/1.1"
"GET /api/recommended_estate/19330 HTTP/1.1"
"POST /api/chair/buy/19330 HTTP/1.1"

また、これらのパターンに加えて、資料請求とイス購入のどちらの得点が多いかも調べました。序盤は 5:1 で資料請求が多め。後半はさらに顕著になって 10:1 ぐらいになっていました。これをもとに、資料請求大事という方向性が見えました。

なぜ nodejs 実装にしたのか?

これは予選突破した今だから言えることだし、とてもとてもおこがましい話なのですが、「Go じゃないと ISUCON は戦えない」というハードルができてしまうのが嫌でした。これは自分たちが ISUCON 9 を迎える時に感じていたハードルでした。結果、Go を選んでも勝てなかった(そもそも書けないんだけど)。Go でもダメなら諦めないとダメかな。そう思った時に、慣れた nodejs 実装のことを考えました。

そこで、これまでの ISUCON を振り返りました。

初めて ISUCON に参加した ISUCON 5 から一昨年の ISUCON 8 まで、ずっと Ruby 実装で挑戦していました。3人とも Ruby ばっかり書いているというタイプではなかったのですが、それなりに書けてました。

ISUCON 5 では奇跡的に予選突破できたのですが、そのあと Ruby で書き続けても、予選突破できない年が続きました。ISUCON 9 まで来て、さすがに Golang にしないと勝てないかな、と思って Golang に切り替えたのですが、まぁ書けない書けない。それなりに予習したつもりでしたが、それでも競技時間中に「これってどうやって書くんだっけ」と調べることが多かったのが反省です。

そして ISUCON 10。正直、一番書けるのは nodejs 実装だというのは確信してました。でもそれで勝てるのか?というのが不安でした。実際、nodejs で予選突破したチームは非常に少ないです。

これで大丈夫なんだろうか……

そんな心配をしながら、昨年予選の nodejs 実装を開いてみると、なんと TypeScript 実装。これは捗る!しかし手元でベンチマークしてみると Go や Ruby の半分程度のスコア。これは厳しいか……と思ったのですが cluster で fork したらあっけなく他言語に並びました。これは行ける!!!!

そう考えて今年の ISUCON 10 に挑み、なんとか予選突破できました。

これで「nodejs でも予選突破できるんやで」って胸を張って言える……そう思っていました。

Nodejs  2組   6.5%

ISUCON10 オンライン予選の利用言語比率 : ISUCON公式Blog

他にもいたんだね。どこのチームだったのかな?

fccpc.hateblo.jp

ponyopoppoが普段から使っているNode.jsです。

( ゚д゚)……………

「nodejs で予選1位通過しました」に勝てるわけないやーん。

ま、それを証明するのは自分じゃなくてもいいよね。nodejs でも ISUCON 戦えるぞ!立ち上がれ!nodejs の民よ!来年の ISUCON で待ってるぞ!

「ダブルチェックの有効性を再考する」をプログラマ視点で読んだ

このツイートで見かけたのがきっかけで、ダブルチェックの有効性を再考する (PDF)を読みました。プログラマ視点で。エンジニア視点って書くと主語広くなっちゃうので、今日はあえてプログラマ視点という表現にしてます。

まず、そもそもこの資料での「ダブルチェック」は医療現場での話で、現場猫が出てくるような工事現場やシステム開発の現場じゃないです。

また、ソフトウェア工学の論文などに首を突っ込めば、ここで出てくる話の検証などがあるのかもですが、そこまでは調べていません。

小さいリスクは整理して捨てる話

これはスライド中の「小さいリスクは整理して捨てる」(p.39) といった表現や、それを具体的に言った「有害事象につながらないエラーは発生しても許容しましょう」(p.55) という話のことです。ただ楽をしましょうということではなくて、大きな事故を減らすために何ができるかという話です。大きな事故が減った様子は p.59 にあります。

話題がダブルチェックなので、コードレビューと対比してしまうのですが、そちらに置き換えると、CSS を1行1行チェックする行為がしっくりきます。念入りにチェックしている方の気分を害したらすみません。

自分が CSS をレビューするとき、見た目に問題がなくて、コードもざっと見て大丈夫そうなら割と LGTM 出しています。例えば .foo, .bar { color: red; } と書いてあって、この .barセレクタが実は使われていないということもあるかもですが、そういったところまで 100% 確認しているかというと、できていないと思います。

他にも確認すべき大事なところがあるならば、細かいところよりもそっちに時間をかけたほうがいいでしょう。

確認エラーの分類

p.12 からの「ダブルチェックしたのになぜ?」で、確認エラーの分類というものが出てきます。「うっかり」「失念」「思い込み」「ルール違反」の4つに分かれていて、プログラミングではどう分類されるか考えてみました。

  • うっかり
    • typo
    • 不等号の向きを間違える
  • 失念
    • リソース解放忘れ
    • 仮データをそのままコミット
  • 思い込み
    • typo ではなく本人が正しいと思っている英語間違い(registUser, denyed など)
    • 「ドキュメント修正」と書かれたコミットをよく見ずにマージしたけど、よく見たらドキュメント以外に意図しない変更が入っていた
  • ルール違反

なんでルール違反を?と思うところですが、時間がなかったり、面倒でさぼってしまったりで発生するのでしょう。後で戻すつもりの一時的なルール違反をそのままコミットしてしまうのは、失念の方に分類されそうです。

ダブルチェックとペアプロと不具合

ダブルチェックの現場の様子が何度か出てきますが、2人で一緒に作業をしているという点ではペアプロに近いように感じられました。

個人的には、ペアプロは開発効率を上げるためのもので、不具合を減らしたりするものではないと考えています。結果的に減るかもしれないけど、それを目的としているわけではない、ということです。

有効なダブルチェックをするなら、同席同時ではなく独立がいいとありますが (p.28)、これはプログラミングでも同じだと思います。

その他雑多な話

  • 「誤薬防止のための6R」なるものがあるそうで、プログラマ的にも「正しい計算量」「正しい権限」みたいに書けると教育に良さそう
  • ダブルチェックは、エラーそのものを減らすのではなく、発見するため
  • James Reason のエラーの定義では「偶然の作用に起因しない」。Wikipediaヒューマンエラーにもそう書いてあった
  • 話題のトリプルチェックの図は「人間による防護の多重化の有効性」がソース。日本品質管理学会の、島倉、田中による論文
  • 最初読んだときは SLO との類似性を求めていたけど、読んでいったらちょっと違う感じだった

以上、プログラマが読んでも色々と考えさせられる資料でした。

ウェブサービスなら多少のエラーは許容する戦略がありうるけど、医療現場でそれはできないだろうな……と思ったら「有害事象につながらないエラーは発生しても許容しましょう」という結論だったのにはびっくりしました。でも実際に効果が出ているようで安心しました。