PythonでクラスC内の型ヒントにC自身を指定するとNameErrorが発生する問題の対処法

問題

以下のようにあるクラスCのメソッドfの型ヒントにC自身を使いたいとします。

class C:
    def f(self) -> C:  # causes NameError!
        return C()

このコードをそのまま実行すると以下のようにNameErrorが発生します。

$ python foo.py
Traceback (most recent call last):
  File "/Users/mickey/foo.py", line 1, in <module>
    class C:
  File "/Users/mickey/foo.py", line 2, in C
    def f(self) -> C:
NameError: name 'C' is not defined

原因

メソッドfが定義される時点でクラスC自体の定義が完了していないため、この時点で型名Cを解決できないことがNameErrorの原因です。

解決策

以下のように型ヒントのCをクォートで括ると解決します。

class C:
    def f(self) -> 'C':  # quoted, causes no error
        return C()

これはForward Referencesと呼ばれるもので、型名を文字列リテラルで指定することで型名の解決を後回しにすることができます。

参考資料

「FIRE 最強の早期リタイア術 最速でお金から自由になれる究極メソッド」を読んだ

FIREとはFinancial Independence, Retire Earlyの略で、簡単に言うと十分な不労所得を確保することで経済的自立し、仕事を早期リタイアして自由な時間を手に入れることです。自分は今のところ早期リタイアするつもりはないのですが、実際に早期リタイアした人の話を知ることでお金について新しい考え方を身につけられるかもしれないと思い、本書を手に取ってみました。

本書の内容

箇条書きで軽くまとめてみます。

  • 基本として貯蓄・節約によって貯めた資産を元手にインデックスファンドへ投資し、資産を増やして経済的自立を目指すことになる。
  • POTスコア=(給与の中央値−最低賃金の差額)÷ 学位にかかる総費用
  • POTスコアが高い(学位が収入に与える費用対効果が大きい)キャリアを選ぶことが大切。
  • 家は様々なコストがかかるので買わない。可能な限り賃貸にする。
  • 車も買わない。
  • 借金はしない。既に借金がある場合は最優先で完済する。複利の力を敵に回すと危険。
  • インデックスファンドは手数料が安く、85%のアクティブファンドを上回る運用成果を出す。
  • 現代ポートフォリオ理論に従ってポートフォリオを設計する。
    • アセットアロケーション(株式や債券などの割合)を決める。例:株式6割、債券4割
    • 投資対象のインデックスを決める。S&P500、FTSE Global All Cap Indexなど。特定の国・地域(特に自国)にオーバーウェイトしすぎないように気をつける。
    • ファンドを選ぶ。投資信託かETF。手数料を抑える。
  • リバランシング:市場の高騰や暴落によって資産の割合があらかじめ決めておいたアセットアロケーションから大きく逸脱し始めた場合、割合が大きくなりすぎた資産を売却し、割合が少なくなりすぎた資産を購入する。
    • 例:「株式6割、債券4割」のポートフォリオが株価の下落によって「株式3割、債券7割」になった場合、債券を売却し、株式を購入することで元の「株式6割、債券4割」に戻す。下落時に株式を安く買い増すことで後の回復期に早く資産が復活することが期待できる。
    • リバランシングは機械的に行う。感情に流され下落時に株を売って回復期に利益を逃すようなことがあってはならない。
    • 株式100%のアセットアロケーションだとリバランシングができない。株式以外の資産もポートフォリオに入れておく。
    • 個別株をポートフォリオに入れてはいけない。インデックスファンドと違い、個別株はゼロ(倒産)になり得るため、株価下落時のリバランシングで個別株を買い続けると資産がなくなる可能性がある。
  • 4%ルール:運用しているポートフォリオの4%の資金で1年の生活費を賄えれば、毎年4%取り崩しても95%の確率で貯蓄が30年以上持続する、という研究結果がある。
    • ポートフォリオの4%の資金で1年生活できるようになるために、25年分の生活費相当のポートフォリオを作り上げる必要がある。
  • 早期リタイアまでの年数は年収ではなく貯蓄率によって決まる。貯蓄率が上がれば投資金額も増え、リターンの複利の力によって早く目標の金額に到達できる。
    • 貯蓄率を上げるために収入だけでなく節約も重要となる。
    • 数パーセントの貯蓄率の変化だけでリタイアまでの年数が数年も変わり得る(10%等低い貯蓄率の場合に顕著)。
  • シークエンス・オブ・リターン・リスク:4%ルールは5%の確率で失敗する。退職後の最初の数年に下落相場が発生すると、資産が大きく目減りしていく中でも生活費のために資産を売却しなければならず、ポートフォリオを大きく毀損してしまい、相場が回復局面に入っても資金を取り戻せなくなる。
    • 対策として「現金クッション」と「利回りシールド」で備える。
  • 現金クッション:下落相場になった時に生活費として使う現金。下落相場時に生活費のためにポートフォリオを大きく取り崩す必要がなくなる。
    • 過去に株式市場が暴落から立ち直るまで最大5年(世界恐慌時)かかっている。
    • そのため5年分の生活費の現金クッションがあると安心。
    • しかしポートフォリオとは別に5年分の現金を用意するのはかなり大きな金額となる。対策として「利回りシールド」を利用する。
  • 利回りシールド:一時的にポートフォリオの資産の中心を高利回り資産に置き換え、分配金を増やす。
    • 投資信託やETFの分配金を現金クッションの一部として使うことで、現金クッションのために用意する資金を抑えられる。
    • 高利回り資産には、優先株、REIT(不動産投資信託)、社債、高配当株のインデックスファンドなどがある。
    • ボラティリティが上がるという欠点がある。
  • 地理的アービトラージ:物価が高い国に住んでいる人は、東南アジアなど物価が安い国に移住したり一年中旅行している方が年間の支出を抑えられる。
  • 子育てしている人でも経済的自立したり年中旅行したりしている人達もいる。
  • 早期リタイア後も副業を始めたりパートタイムで仕事に戻るという選択肢も考慮する。フルタイムの仕事よりも自由な時間を確保しつつ資産にも余裕ができる。

感想など

  • 著者がカナダ在住のため税金などの面で日本と違う点があるが、アイディア自体は日本でも概ね実行可能だと思う。
  • 節約重要。早期リタイアを目指すかどうかはともかく自分の支出を詳細に把握してみようと思った。
  • フルタイムで働いている間は分配金を受け取るよりも投資信託で分配金を自動再投資しつつ資産の増加に期待したほうがいいと思う。しかし退職時に高分配のETFや投資信託へ鞍替えすると投資信託の売却時に課税されてしまうため悩ましい。
  • カナダではETFの方が手数料や経費率の面で投資信託よりも有利らしくETF一択とのことだが、日本だと海外ETFの外国税額控除の手続きが必要だったり為替手数料を考慮する必要があったりするため一概にどちらがいいか言えないと思う。
  • 利回りシールドは生活費用の現金(の一部)を分配金に頼る戦略だが、下落相場時に投資信託やETFの分配金が著しく下がる、または支払われない可能性はどれくらいあるだろうか?
  • 前半の著者の育った環境の話(中国で1日44セント生活)やお金に関する持論(お金は世界で最も大切)は単純に興味深かった。

終わりに

分配金の下落の可能性など、実践する上でいくつか疑問点は残りますが、早期リタイアへの道を可能な限り再現性のあるやり方で紹介している点は評価できると思います。FIREに興味がある方は読んでみて損はないのではないでしょうか。

ちなみに著者は「お金は世界で最も大切」と言及していましたが、「金は命より重い・・・!そこの認識をごまかす輩は、生涯地を這う・・・!!」とは言っていませんでした。

最近の自分のC++スタイル

何だかんだでC++を書き続けています。今やC++20です。C++0xで騒いでいた頃が懐かしいです。

今回は最近自分がC++を書く時に気にしていることについて簡単に紹介していこうと思います。

基本方針

同じコードを触る開発者が多いため、実装者にとっての利便性よりも共同開発者にとっての可読性や保全性を重視しています。そのため、可読性を下げる機能や扱いが難しい機能は扱わない事が多いです。

auto

autoは書き手にとっては便利ですが、多用すると(特に共同開発者にとっての)コードの可読性が下がるためあまり使いません。

// Preferred: articlesの型がひと目で分かる
std::optional<std::vector<CrawledArticle>> articles = CrawlArticles(...);

// CrawlArticles()のシグネチャーを調べない限りarticlesの型が分からない
auto articles = CrawlArticles();

ただし、コンテナのイテレーターは型名が無駄に長く、また変数の型が周辺のコードから明らかなことが多いためautoを使っています。 同様の理由で、std::make_uniqueの戻り値を受ける変数に対してもautoを使っています。

// Preferred: std::make_uniqueの戻り値の型は自明なのでautoを使っても可読性は失われない
auto crawler = std::make_unique<ArticleCrawler>(...);

// autoを使わないとArticleCrawlerが二度出てくるため冗長に見える
std::unique_ptr<ArticleCrawler> crawler = std::make_unique<ArticleCrawler>(...);

std::pairとstd::tuple

operator<などが定義されているため便利ですが、first、secondやget<3>はdescriptiveな名前ではなく、可読性が下がるため極力使わないようにしています。可能であればstructを定義した方が可読性が高いコードになります。

// secondが何を保持する変数なのかひと目で分からない
if (ProcessArticle(article_info.second)) {...}

// (0オリジンで)2番目の要素が何か調べないとコードを把握できない
if (ProcessArticle(std::get<2>(article_info))) {...}

継承

ポリモーフィズムのための継承はほぼ使っていません。たまにStrategyパターンのために使うことがある程度ですが、多用すると他の開発者にとっての可読性が下がる(オブジェクトがインスタンス化されている箇所まで辿らないと仮想関数呼び出しがどの実装を呼んでいるのか分からなくなる)ため気をつけています。Template Methodパターン等、実装を共有する目的での継承は使っていません(共同開発者で使っている人はいます)。

Dependency Injectionでユニットテストをしやすくする目的での継承は使っています。Abstract Factoryも稀に使います。ただし、具象クラスを使ったテストで済む場合(ネットワークやディスクアクセスを起こさず、動作も十分速いクラスに依存するコードの場合)はなるべくDependency Injectionを使わずにテストしています。

テンプレート

テンプレートは強力ですが、共同開発者がコードを読む時に関数テンプレートやクラステンプレートがインスタンス化されている箇所まで辿らないと動作の確認ができない場合があるため多用しないようにしています。ただし、他の人も色々な型の変数で使いたくなるようなライブラリを設計する場合は話が変わってくると思います。

std::unique_ptr

非常によく使います。生のポインタをnewすることはほぼありません。std::unique_ptrをインスタンス化する時はstd::make_uniqueで行います。std::unique_ptrのコンストラクタはほぼ使いません。

std::unique_ptrは関数のシグネチャー上でオブジェクトの所有権の移動を明確化するためにも使えます。可読性や保全性の向上が期待できます。

class Notifier {
 public:
  // 引数がstd::unique_ptrの場合は呼び出し元がオブジェクトの所有権を譲渡することを意味する
  explicit Notifier(std::unique_ptr<MailSender> mail_sender)
    : mail_sender_(std::move(mail_sender)) {}
  ...
 private:
  std::unique_ptr<MailSender> mail_sender_;
};
...
auto mail_sender = std::make_unique<MailSender>(...);
...
// std::unique_ptrの指すオブジェクトの所有権をstd::moveで譲渡する
auto notifier = std::make_unique<Notifier>(std:move(mail_sender));
// 戻り値がstd::unique_ptrのため、呼び出し元が戻り値の所有権を受け取ることを意味する
std::unique_ptr<ArticleCrawler> CreateArticleCrawler();

ちなみにstd::shared_ptrは今のところ滅多に使っていません。

Designated initializers

structの初期化で便利かつ可読性も向上するのでよく使います。日本語では指示付き初期化と言うらしいです。

struct Person {
  std::string name;
  int age = 0;
};

Person person = {.name = "John Doe", .age = 42};

指示付き初期化は元々C99で導入された機能ですが、C++20でも利用可能になりました。ただしいくつかC++20特有の制約があります(こちらのページの"仕様"を参照してください)。

他にも、指示付き初期化は引数が多いorデフォルト引数の一部だけを変えて呼び出すことが多い関数に使うと便利です。 abseil / Tip of the Week #173: Wrapping Arguments in Option Structsに載っている例を少し簡略化したものを以下に紹介します。

struct PrintDoubleOptions {
  std::string_view prefix = "";
  int precision = 8;
  char thousands_separator = ',';
  char decimal_separator = '.';
  bool scientific = false;
};

void PrintDouble(double value,
                 const PrintDoubleOptions& options = {});

// 変更したいオプションだけ指定する
PrintDouble(5.0, {.prefix = "value=", .scientific = true});

structと指示付き初期化なしだと以下のように扱いづらいコードになります。

void PrintDouble(double value, std::string_view prefix = "", int precision = 8,
                 char thousands_separator = ',', char decimal_separator = '.',
                 bool scientific = false);

// 引数リストの後半にあるものだけ変えたい場合、その引数よりも前のデフォルト引数を全て指定しなければならない
// 同じ型の引数が隣り合っていると順番を間違えてもコンパイルが通ってしまう
//(この場合thousands_separatorとdecimal_separator)
PrintDouble(5.0, "value=", 8, ',', '.', true);

終わりに

他にもプロジェクト独自の規約や自分なりの基準がありますが、最近よく気にしているのはだいたいこの辺りです。

pytype:型ヒントなしでも使えるPython用静的型検証ツール

github.com

PytypeはGoogle製のPython用静的型検証ツールです。型ヒントなしのコードでも型推論を行って検証し、潜在的なバグを指摘してくれます。

インストール

pip install pytype

使用例

例えば以下のようなfoo.pyというファイルがあったとします。

def increment(x):
    return x + 1

increment('0')

このコードは実行するとTypeErrorをraiseします。

pytypeでこのコードを検証してみます。

$ pytype foo.py

すると、以下の通り当該コードに対してunsupported operand type(s) for +: 'str' and 'int'というエラーを出力します。実引数と関数本体のコードを組み合わせた検証もしてくれることが分かります。

File "/Users/mickey/depot/pytype_examples/foo.py", line 2, in increment: unsupported operand type(s) for +: 'str' and 'int' [unsupported-operands]
  Function __add__ on str expects str
Called from (traceback):
  line 4, in current file

pytypeは型推論以外にも、未定義属性の参照など様々なプログラムエラーをレポートしてくれます。

class Person(object):
    def __init__(self, name):
        self.name = name
        # self.ageを定義していない

    def greet(self):
        # 未定義のself.ageを参照している
        print("Hi, I'm {}. I'm {} years old.".format(self.name, self.age))
$ pytype foo.py
File "/Users/mickey/depot/pytype_examples/foo.py", line 8, in greet: No attribute 'age' on Person [attribute-error]

その他の使い方はGithubのプロジェクトページを参照してください。例えば、pytypeが解析した型情報を型ヒントとして出力し、ソースコードにmergeすることも可能です。

まとめ

pytypeは型ヒントなしのコードでも型推論を行い検証してくれます。実際にコードを実行する前に潜在的なバグを素早く確認できるため、ユニットテストの前に実行させるようにしておくと便利です。

「a=10とb=5の平均は7.5です」をmain関数なしで表示する

発端

課題

Int a=10; Int b=5; と代入し平均を表示する

「a=10とb=5の平均は7.5です」と画面に表示する

f:id:mickey24:20190710115023p:plain:w300

考察

  • 「Int」と書いてあるがこれはtypedef int Int;が必要?とりあえず普通のintでもよいと解釈する。
  • 「Int a=10; Int b=5; と代入し」と言っているが、「代入後aとbを使わなければならない」とは言っていない。
  • 「平均を表示する」とは言っているが「平均を計算する」とは言っていない。
  • 「main関数を書く必要がある」とは言っていない。
  • 「どの環境でも実行できる必要がある」とは言っていない。

int a = 10;
int b = 5;
const char main[] = "\x48\xc7\xc0\x01\x00\x00\x00\xba\x20\x00\x00\x00\xbf\x01\x00\x00\x00\xe8\x0c\x00\x00\x00\x48\x81\xc6\x11\x00\x00\x00\x0f\x05\x31\xc0\xc3\x48\x8b\x34\x24\xc3\x61\x3d\x31\x30\xe3\x81\xa8\x62\x3d\x35\xe3\x81\xae\xe5\xb9\xb3\xe5\x9d\x87\xe3\x81\xaf\x37\x2e\x35\xe3\x81\xa7\xe3\x81\x99\n";

なにこれ

これはC言語で書かれたプログラムです。実行すると「a=10とb=5の平均は7.5です」とstdoutに出力されます。x64の一部の環境でしか動作しません。実行結果はこちら:https://wandbox.org/permlink/bWtd0lfQn0ErFgG9

おそらくこの課題に取り掛かった大学生のうち約7〜8割はこの解にたどり着くのではないかと思います。

なにこれ (1)

一見ランダムに見えるmain配列の文字列は「機械語」の命令に相当するもので、以下のような構成になっています。

int a = 10;
int b = 5;
const char main[] =
    // mov $0x01,%rax
    "\x48\xc7\xc0\x01\x00\x00\x00"
    // mov $0x20,%edx
    "\xba\x20\x00\x00\x00"
    // mov $0x01,%edi
    "\xbf\x01\x00\x00\x00"
    // callq 0x0c
    "\xe8\x0c\x00\x00\x00"
    // add $0x11,%rsi
    "\x48\x81\xc6\x11\x00\x00\x00"
    // syscall
    "\x0f\x05"
    // xor %eax,%eax
    "\x31\xc0"
    // ret
    "\xc3"
    // mov (%rsp),%rsi
    "\x48\x8b\x34\x24"
    // ret
    "\xc3"
    // "a=10と"
    "\x61\x3d\x31\x30\xe3\x81\xa8"
    // "b=5の"
    "\x62\x3d\x35\xe3\x81\xae"
    // "平均は"
    "\xe5\xb9\xb3\xe5\x9d\x87\xe3\x81\xaf"
    // "7.5です\n"
    "\x37\x2e\x35\xe3\x81\xa7\xe3\x81\x99\n";

このようなデータをmainという名前の配列に保持しておくことで、crt0がプログラム開始後にmainという名前が指している場所にあるデータを機械語の命令として実行してくれます。出力される文字列「a=10とb=5の平均は7.5です」はUTF-8バイト列の16進表記としてmain配列の末尾に格納してあります。

実際に実行される処理は以下とほぼ同等です。

write(1, "a=10とb=5の平均は7.5です\n", 32);

各機械語の命令の詳細は以下の記事が非常に参考になります。

koturn.hatenablog.com

感想

C言語はプログラマーに「main関数を書かない」という選択肢も与えてくれて自由度が高くていいなあとおもいました(白目)。

Cにおいてconst char main[]でプログラムを書けることは5年以上前から知っていましたが、実際にやってみたのは今回が初めてです。記号プログラミングもそうですが、言語仕様や処理系の実装の秘孔を突くのはやっぱり楽しいものです。GoやPythonといった最近の言語だとなかなかこういう遊びができないので、C言語はまだ人類に必要だと痛感しました。

参考文献

「仕事ではじめる機械学習」は仕事における機械学習プロジェクトの進め方を教えてくれる良書

仕事ではじめる機械学習

仕事ではじめる機械学習

良書だと思います。

どのような本?

「機械学習本」というと、機械学習の理論の話やTensorFlowのようなフレームワークにフォーカスした本を思い浮かべることが多いと思います。それに対し、本書は「仕事で使う」という観点から機械学習プロジェクトに進めていくためのノウハウが語られています。

実際に機械学習プロジェクトで持続可能な成果を出すためには、本書で触れられているようなシステム設計や運用に関する知識も非常に重要です。

f:id:mickey24:20190710104336p:plain

参考になった点

主に「1.2.2 機械学習をしなくて良い方法を考える」「1.3 実システムにおける機械学習の問題点への対処法」「4.2 システム設計」「6章 効果検証」が面白かったです。

機械学習を使わないことを恐れない

「1.2.2 機械学習をしなくて良い方法を考える」では機械学習を用いるシステム構築の難しさが触れられています。具体的には以下の問題点が紹介されています。

  1. 確率的な処理があるため自動テストがしにくい
  2. 長期運用しているとトレンドの変化などで入力の傾向が変化する
  3. 処理のパイプラインが複雑になる
  4. データの依存関係が複雑になる
  5. 実験コードやパラメータが残りやすい
  6. 開発と本番の言語/フレームワークがバラバラになりやすい

これらの点から、例えば簡単なルールベースの実装でビジネス上の目標を達成できるシステムが構築できるのであれば、機械学習を用いないほうが技術的負債が蓄積しにくくなり長期的な運用が容易になります。

たった2ページだけの節ですが、ここで触れられている機械学習ありのシステム構築が難しい理由は常に念頭に置いておく価値があると思います。プロダクトをリリースしてビジネス上の目標を達成することが目的で、機械学習はそのための道具のひとつなのであって、機械学習を使うこと自体が目的であるべきではありません。

機械学習を導入したシステムの問題点を対処する

「1.3 実システムにおける機械学習の問題点への対処法」では、機械学習を導入することが正式に決まったプロジェクトにおいて、上述の機械学習システムの問題点に対処するためのポイントが触れられています。

  1. 人手でゴールドスタンダードを用意して、予測性能のモニタリングをする
  2. 予測モデルをモジュール化してアルゴリズムのA/Bテストができるようにする
  3. モデルのバージョンを管理して、いつでも切り戻し可能にする
  4. データ処理のパイプラインごと保存する
  5. 開発/本番環境の言語/フレームワークは揃える

これらの中にはプロジェクトの早い段階で計画しておかないと後から着手することが難しいものもあるため、機械学習プロジェクトを始める時点で検討しておくことが重要です。

学習・予測用システムの設計パターンを知る

「4.2 システム設計」では、学習と予測の各フェーズのシステム構成パターンと、それぞれの長所・短所について触れています。

  1. バッチ処理で学習+予測結果をWebアプリケーションで直接算出する(リアルタイム処理で予測)
  2. バッチ処理で学習+予測結果をAPI経由で利用する(リアルタイム処理で予測)
  3. バッチ処理で学習+予測結果をDB経由で利用する(バッチ処理で予測)
  4. リアルタイム処理で学習する

おそらく小規模サービスの場合や予測処理が重すぎない場合は1か2の設計を思い浮かべる場合が多いと思います。しかし、Webアプリケーションと機械学習モデルを疎結合にしつつ予測のレイテンシーを抑えたい場合は、3のようにバッチ処理で予測結果をDBに格納し、WebアプリケーションでDB内の予測結果を利用する方法が非常に有効です。ただし、バッチ予測中に出てこなかった入力データに対しても何らかの予測結果を出したい場合は、DBのkeyのフォーマットやlookup方法の工夫が必要になります。

リリースしたシステムの効果検証を適切に行う

「6章 効果検証」では、リリースしたシステムによってもたらされた効果を検証するための手法や注意点が紹介されています。

機械学習モデルを作る過程でのモデルの評価指標(precision、recall、F値、AUC等)とは異なり、機械学習モデルのリリースによる利益貢献などビジネス上の効果はオフラインで検証することが非常に困難です。そのため、リリース後のユーザーの行動ログなどを基に、仮設検定、因果推論、A/Bテストなどを利用して効果を検証する必要があります。

ただし、「A/Bテストなど逐次データが得られる場合に標本を変えながら仮説検定を繰り返してしまうと無益な施策でもいつか有意差が出てしまう」「強い季節性を持っているログから因果推論を行う場合はリリース前後のデータをそのままできない」など、誤った効果検証結果を導き得る落とし穴が存在するため、注意が必要です。

もっとカバーして欲しかった点

第Ⅱ部のケーススタディは参考にはなるのですが、プロジェクト初期段階でのデータ分析や機械学習用データセットの構築、機械学習モデルの構築といった内容に重点が置かれていて、欲を言えば本番環境での導入・運用や効果検証についてもう少し具体的な話が触れられていると良かったかなと思います。

例えば予測結果をDB経由で利用する話、モニタリングの方法、A/Bテストを実施する上で必要なフレームワークの実装やログ設計、観測されたコンバージョンレートに基づく利益貢献の仮説検定や因果推論などがある程度具体例を交えて紹介されているともっと面白かったと思います。

まとめ

機械学習を用いるシステムは機械学習なしのシステム以上に運用が難しく、Googleが公開している機械学習プロジェクトのベストプラクティス集 "Rules of Machine Learning: Best Practices for ML Engineering" でも、一番最初に出てくるルールは "Rule #1: Don’t be afraid to launch a product without machine learning"(機械学習なしでプロダクトをローンチすることを恐れるな)とあるほどです。それでも機械学習を使わざるを得ないようなシステムに取り組む場合、本書に載っているノウハウは機械学習プロジェクトを進めていく上できっと役に立つと思います。

本書は220ページ程と比較的コンパクトで読みやすいですが、その分浅く広くなところもあるので、もっと深く知りたいトピックに関しては本書で紹介されている参考文献を参照してみようと思っています。

バイオリン図で数値データの分布を可視化する

今更ながらデータの分布を比較する図法「バイオリン図(violin plot)」の存在を知りました。

バイオリン図とは

f:id:mickey24:20190621232457p:plain

↑のような図です。数値データの分布の可視化や比較に使います。データ分布の描画にはカーネル密度推定が用いられています。

Matplotlibではviolinplot()関数を使うことで描画できます。

matplotlib.pyplot.violinplot(
    dataset,
    positions=None,
    vert=True,
    widths=0.5,
    showmeans=False,
    showextrema=True,
    showmedians=False,
    points=100,
    bw_method=None,
    *,
    data=None)

バイオリン図と箱ひげ図を比較してみる

似たような目的で用いられる図として「箱ひげ図(box plot)」があります。ここでは試しに数値データの分布をバイオリン図と箱ひげ図で描画して比較してみましょう。

まずは分布比較用として3種類の人工データ(単峰性、二峰性、一様分布)を作ってみます。

import numpy as np
import matplotlib.pyplot as plt

np.random.seed(24)

data = [
    # 単峰性
    np.random.normal(scale=2, size=1000),
    # 二峰性
    np.r_[
        np.random.normal(loc=-2, size=500),
        np.random.normal(loc=2, size=500),
    ],
    # 一様分布
    np.random.uniform(-5, 5, size=1000),
]

各データのヒストグラムを個別に描画して分布を確認してみます。

_, axes = plt.subplots(nrows=len(data), ncols=1, figsize=(5, 10))
for i, x in enumerate(data):
  axes[i].hist(x, bins='auto')
plt.show()

f:id:mickey24:20190621232437p:plain

想定通りの分布のデータが作られたことが確認できました。

では、箱ひげ図で各データの分布を比較してみます。

plt.boxplot(data)
plt.show()

f:id:mickey24:20190621232508p:plain

二峰性や一様分布などの特徴が完全に消え、あたかも各分布が同じ特徴を持った分布であるかのように見えます。そもそも多峰性の分布や異なる形の分布を箱ひげ図で比較すること自体よくないですが、ある程度データの分布の形を仮定したまま箱ひげ図から描き始めてしまうと、今回のような図によって誤解を招く可能性があるので気をつけましょう。

今度はバイオリン図で分布を図示してみます。

plt.violinplot(data, showmedians=True)
plt.show()

f:id:mickey24:20190621232457p:plain

箱ひげ図と違い、各データの分布がそのまま図に現れているため、より正確な比較ができます。強いて言えば、カーネル密度推定により一様分布の両端が実際よりも滑らかに描画されているため、カーネル密度推定が苦手とする形の分布を扱う場合は気をつけたほうが良いでしょう。

バイオリン図の欠点

バイオリン図の欠点について考えてみます。

  • データ分布はカーネル密度推定プロットによって滑らかに描画されるため、実際にはデータが存在しない範囲にもあたかもデータが存在しているかのように見えることがある。
  • ひとつのバイオリン図内にある各カーネル密度推定プロットの面積は同一ではない。値が取りうる範囲が大きかったりデータが一様に分布していると、描画されるカーネル密度推定プロットの面積が大きくなるため錯覚が起こる可能性がある。
  • 知名度が低い(Wikipedia談)。しかし割と直感的に分かりやすい図法ではあると思われるので、知名度が低くても人から理解されにくいということはなさそう?

まとめ

数値データの分布を可視化・比較したい場合は、バイオリン図を使ってみましょう。単峰性のデータかつ四分位数や外れ値が重要な場合は箱ひげ図でもいいかもしれませんが、単峰性データの歪み具合を見たい程度であればバイオリン図にしてしまってもいいのではないかと思います。

参考文献