2020年のまとめ

今年も一年間お世話になりました。
多くの方に訪問いただけていたのでモチベーションを失わずにブログ更新を続けることができました。

2020年最後の投稿になりますので、今年1年間の振り返りをしたいと思います。

まず基本的なデータ。記事数はトータル、残り二つは2020年1年間の実績です。

– 記事数 409記事 (この記事含む)
– 訪問ユーザー数 146,674人
– ページビュー 258,698回

2019年と比べて、どの数値も大きく伸びていることがわかります。
更新頻度こそ2019年から落としていますが、更新を続けることによって(結果的に記事にしなかった内容も含めて)多くの学びを得ることができた1年間でした。

さて、恒例のよく読まれた記事ランキングをみていきましょう。
今回は2020年1年間でのPV数によるランキングです。

  1. matplotlibのグラフを高解像度で保存する
  2. macにgraphvizをインストールする
  3. kerasのto_categoricalを使ってみる
  4. numpyのpercentile関数の仕様を確認する
  5. scipyで階層的クラスタリング
  6. DataFrameを特定の列の値によって分割する
  7. INSERT文でWITH句を使う
  8. matplotlibのデフォルトのフォントを変更する
  9. pythonで累積和
  10. Pythonで多変量正規分布に従う乱数を生成する

あれ、今年書いた記事があんまりランクインしてないような。
基本的な内容の記事がニーズが高い傾向にはあるようですね。

僕自身の備忘録的な記事も多く、これは本当に訪問される皆さんの役に立ってるのかと心配になることも多いのですが、
アクセスを見る限りではちゃんとニーズがあったようで嬉しいです。
無理して内容のレベルを上げるよりも今の調子での更新が良いのかもしれませんね。

さて、年初に立てた目標の方も振り返ってみると、更新回数以外の目標はさっぱりでした。
参考: 2020年のご挨拶と今年の目標
Kaggleはそのうち挑戦しようと思いつつ全然やってないし、
技術記事以外の記事もさっぱり書いていません。
技術記事だけで100記事書けたと言うことでもあるので、それが悪いわけではないのですが当初思ってたのとは方向がずれたかなと思います。

来年どのくらい記事を書くかとか、その内容とかはブログ以外の目標やプランともすり合わせて計画を立てて、
年初の記事で方針発表させていただけたらと思います。

今年も1年間ありがとうございました。良いお年を。

シンプソンのパラドックス

先日、データを分析している中でシンプソンのパラドックスが発生しているのを見かけました。
興味深い現象なので、紹介したいと思います。
ただし、業務的な情報は書けないので記事中の用語も設定もデータも全部架空の物です。

2種類のアプリがあったとします。それぞれ旧アプリと、新アプリとします。
そしてそれらのアプリを使っているユーザーがとある属性によってグループA,グループBに分かれていたとします。

ユーザー数の内訳が次のようになっていたとします。(単位:人)

旧アプリ 新アプリ
グループA 40000 1000
グループB 60000 9000

これらのユーザーのコンバージョン数が次の通りだったとします。

旧アプリ 新アプリ
グループA 3200 100
グループB 1800 360

コンバージョン率を見ると次のようになりますね。

旧アプリ 新アプリ
グループA 8% 10%
グループB 3% 4%

どちらのグループのユーザーに対しても、新アプリの方がコンバージョン率が高いことがわかりました。

しかしここで、グループごとに分けて集計することをやめて、新アプリと旧アプリを単純に比較してみます。

旧アプリ 新アプリ
ユーザー数 100000 10000
コンバージョン数 5000 460
コンバージョン率 5% 4.6%

なんと、新アプリより旧アプリの方がコンバージョン率が高いことになりました。

このように、
集団全体を複数の集団に分けてそれぞれの集団で同じ仮説(今回の例では新アプリの方がコンバージョン率が高い)が成り立っても、
集団全体に対してはそれが成り立たないことがあることをシンプソンのパラドックスと呼びます。

これは$\frac{a}{A}>\frac{b}{B}$ かつ $\frac{c}{C}>\frac{d}{D}$ が成り立ったとしても
$$\frac{a+c}{A+C}>\frac{b+d}{B+D}$$
が成り立つわけじゃないと言う単純な数学的な事実から発生する物です。

今回の例で言えば、新アプリの方がどちらのユーザー群に対しても良い効果をもたらしているので良さそうなのに、
全体の集計だけで旧アプリの方が良いと結論づけてしまうと誤った分析をしてしまうことになります。
注意する必要がありますね。

gensimでCoherence(UMass)の算出

前回に続いてCoherence(UMass)の話です。
今回は実際にプログラムで算出してみます。

Perplexityの時は、架空の言語で実験していましたが、あのデータではCoherenceを試すのには都合が悪いことが多いので、
久しぶりに 20newsgroups のデータを使います。

とりあえず読み込んで単語区切りの配列にしておきます。
簡単な前処理として小文字に統一し、アルファベット以外の文字は除去しておきましょう。


from sklearn.datasets import fetch_20newsgroups
import re

# データ読み込み
twenty = fetch_20newsgroups(
    subset="all",
    remove=('headers', 'footers', 'quotes')
)
texts = twenty.data

# 簡単な前処理
# 小文字に統一
texts = [text.lower() for text in texts]
# アルファベット以外は除去
texts = [re.sub("[^a-z]+", " ", text).strip() for text in texts]
# 空白で区切り配列に変換
texts = [text.split(" ") for text in texts]

モデルを学習しておきます。


from gensim.corpora.dictionary import Dictionary
from gensim.models import LdaModel


# 単語と単語IDを対応させる辞書の作成
dictionary = Dictionary(texts)
# 出現が20回未満の単語と、50%より多くのドキュメントに含まれる単語は除く
dictionary.filter_extremes(no_below=20, no_above=0.5)
# LdaModelが読み込めるBoW形式に変換
corpus = [dictionary.doc2bow(text) for text in texts]

# トピック数 20 でモデル作成
lda = LdaModel(corpus, num_topics=20, id2word=dictionary)

# 学習結果
lda.show_topics(num_topics=20, num_words=5)
"""
[(0, '0.017*"was" + 0.016*"were" + 0.015*"they" + 0.010*"on" + 0.009*"with"'),
 (1, '0.025*"you" + 0.018*"be" + 0.016*"this" + 0.013*"on" + 0.011*"if"'),
 (2, '0.031*"key" + 0.014*"chip" + 0.013*"s" + 0.013*"be" + 0.013*"this"'),
 (3, '0.019*"he" + 0.016*"s" + 0.014*"game" + 0.013*"team" + 0.012*"was"'),
 (4, '0.154*"ax" + 0.067*"m" + 0.036*"p" + 0.034*"q" + 0.032*"f"'),
 (5, '0.018*"not" + 0.014*"you" + 0.013*"as" + 0.013*"are" + 0.013*"be"'),
 (6, '0.014*"with" + 0.014*"this" + 0.013*"have" + 0.012*"windows" + 0.011*"or"'),
 (7, '0.019*"have" + 0.016*"my" + 0.013*"if" + 0.013*"with" + 0.012*"this"'),
 (8, '0.016*"s" + 0.015*"as" + 0.012*"you" + 0.012*"be" + 0.012*"are"'),
 (9, '0.016*"x" + 0.014*"on" + 0.011*"or" + 0.009*"are" + 0.009*"with"'),
 (10, '0.017*"with" + 0.017*"scsi" + 0.015*"card" + 0.015*"mb" + 0.013*"x"'),
 (11, '0.016*"echo" + 0.014*"surface" + 0.013*"planet" + 0.012*"launch" + 0.011*"moon"'),
 (12, '0.045*"x" + 0.013*"section" + 0.011*"s" + 0.011*"file" + 0.010*"if"'),
 (13, '0.025*"image" + 0.017*"you" + 0.014*"or" + 0.011*"file" + 0.011*"can"'),
 (14, '0.015*"on" + 0.013*"s" + 0.011*"by" + 0.011*"be" + 0.009*"from"'),
 (15, '0.031*"you" + 0.020*"t" + 0.020*"they" + 0.014*"have" + 0.013*"s"'),
 (16, '0.024*"edu" + 0.013*"by" + 0.012*"from" + 0.012*"com" + 0.008*"university"'),
 (17, '0.049*"he" + 0.044*"was" + 0.029*"she" + 0.019*"her" + 0.013*"s"'),
 (18, '0.013*"are" + 0.011*"with" + 0.010*"or" + 0.010*"car" + 0.010*"s"'),
 (19, '0.020*"power" + 0.019*"myers" + 0.014*"g" + 0.014*"e" + 0.012*"period"')]
"""

ストップワードの除去などの前処理をもっと真面目にやった方が良さそうな結果になってますね。。。

一旦今回の目的の Coherence の計算に進みます。
実はこの学習したldaのオブジェクト自体も算出用のメソッドを持っているのですが、
それとは別にCoherenceの計算専用のクラスがあります。

参考: models.coherencemodel
これを使うのが簡単でしょう。


from gensim.models.coherencemodel import CoherenceModel 

# Coherenceの計算
cm = CoherenceModel(
    model=lda,
    corpus=corpus,
    dictionary=dictionary,
    coherence='u_mass', # Coherenceの算出方法を指定。 (デフォルトは'c_v')
    topn=20 # 各トピックの上位何単語から算出するか指定(デフォルト20)
)
print(cm.get_coherence())
# -1.661056661022431

簡単に得られましたね。

前の記事で UMass Coherence はトピックごとに求まると言う話をしました。
それは、get_coherence_per_topic()で得られます。


coherence_per_topic = cm.get_coherence_per_topic()
print(coherence_per_topic)
"""
[
-0.6804875178873847,
-0.7418651773889635,
-2.112586843668905,
-1.5566020659262867,
-1.3794139986539538,
-0.9397322672431955,
-0.9144876198536442,
-1.050640800753007,
-0.791666801060629,
-1.5573334717678569,
-1.7592326101494569,
-2.4339787874196244,
-3.4187325854772057,
-1.4492302603021243,
-0.8756627315871455,
-0.8056235761203832,
-2.121420273613335,
-3.5341207908402237,
-1.1732696265877514,
-3.925045414147548
]
"""

# 平均を計算 (cm.get_coherence()と一致する)
print(sum(coherence_per_topic)/len(coherence_per_topic))
# -1.661056661022431

配列は0番目のトピックから順番にそれぞれのCoherenceを示します。

この他、ldaオブジェクトが持っている、 top_topics() というメソッドでもCoherenceを得られます。

Get the topics with the highest coherence score the coherence for each topic.

というドキュメントの説明通り、本来は、coherenceが高いtopicsを求めるためのものなので注意が必要です。
(高い順にソートされてしまうこととか、topnの引数が小さいと途中で打ち切られることとか。)

何故かこれは coherenceを算出する方法のデフォルトが’u_mass’です。(CoherenceModelは’c_v’なのに。)

このような感じで使います。


lda.top_topics(corpus=corpus, coherence='u_mass', topn=20)

"""
[([(0.017169138, 'was'),
   (0.015623925, 'were'),
   (0.015399945, 'they'),
   (0.010244668, 'on'),
   (0.00926054, 'with'),
   (0.009065351, 'had'),
   (0.0085669095, 'their'),
   (0.008559594, 'by'),
   (0.00840216, 's'),
   (0.008026317, 'at'),
   (0.007591937, 'from'),
   (0.007375864, 'as'),
   (0.0072524482, 'are'),
   (0.007035575, 'have'),
   (0.00656652, 'all'),
   (0.0062510483, 'or'),
   (0.0058769537, 'who'),
   (0.005784945, 'this'),
   (0.0057791225, 'there'),
   (0.0056513455, 'but')],
  -0.6804875178873847),
# 長いので2トピック目以降の結果は略
]
"""

トピックごとのタプルの配列が戻り、タプルの2番目の要素がcoherenceです。
少し扱いにくいですね。

Coherence(UMass)によるトピックモデルの評価

今回もトピックモデルの評価指標の話です。
前の2記事でPerplexityを扱ったので、今回は Coherence を扱います。

さて、トピックモデルの予測精度を評価していたPerplexityに対して、
Coherenceはトピックの品質を評価するものです。

人間が見て、このトピックは何の話題に言及するものだ、とわかりやすく分類できていたらCoherenceが高くなります。
そう説明すると単純なように思えるのですが、これを実現する指標を作るのはなかなか大変です。
そのため、Coherenceの定義はこれ、と明確に定まったものは無く、いろんな手法が提案されています。
gensimに定義されているものだけでも、u_mass, c_v, c_uci, c_npmi と4つがありますし、
実際に提唱されているものは人間が評価するものなどもっとあります。

これらの中で、別のコーパス(Wikipediaのデータなど)を用意しなくてよかったり、Google検索結果などを使わなくても良く、
計算速度も早い u_mass の使い方を紹介します。

提唱された論文はこちらです。
参考: Optimizing Semantic Coherence in Topic Models

どうでもいいのですが、 u_mass が何の略なのかずっと疑問でした。
論文を見ると University of Massachusetts のようですね。
Mimno(2011)のDavid Mimnoさんは Princeton University なのでなぜu_massと呼ばれているのかは謎です。

話を戻します。
論文中で提唱されているCoherenceは次の式で計算できます。

トピック$t$に対して、出現頻度の高い$M$個の単語の集合を$V^{(t)}=\{v_1^{(t)},\dots,v_M^{(t)}\}$とします。
$D(v)$を単語の出現文書数、$D(v_1,v_2)$を単語の共起文書数とするとトピック$t$のCoherenceは次の式になります。
$$
C(t; V^{(t)})=\sum_{m=2}^{M}\sum_{l=1}^{m-1}\log\frac{D(v_m^{(t)}, v_l^{(t)})+1}{D(v_l^{(t)})}.
$$

要するに、そのトピックの頻出単語たちがよく共起してるほど高くなるように作られています。
そのため、トピック数等の決定に使う場合は、Coherenceが高いものを採用します。
(Perplexityは低いものを採用するので注意です。)

定義から明らかですが、この指標は各トピックごとに計算されます。
そのため、モデルの評価として使うには各トピックごとのCoherenceの平均を使います。

前置きが長くなってきたので、サンプルコードは次の記事で書きたいと思います。
なお、 scikit-learnの方には実装がないようです。
そのため、scikit-learnでLDAを実装した場合は上の式を自分で実装する必要があります。

gensimには実装されているのでそちらを紹介予定です。

gensimのTopicモデルでPerplexityを計算する

前回、scikit-learnのトピックモデル(LDA)における評価指標として、Perplexityを算出する方法を紹介しました。
参考: トピックモデルの評価指標Perplexityの実験

今回はgensim版です。
gensimのLDAモデルには log_perplexity と言うメソッドがあるので、それを使うだけです、って話であれば前の記事とまとめてしまってよかったのですが、
話はそう単純では無いので記事を分けました。

さて、 log_perplexity ってメソッドですが、いかにも perplexity の自然対数を返してくれそうなメソッドです。
perplexity が欲しかったら、 $\exp(log\_perplexity)$ を計算すれば良さそうに見えます。
しかし、 log_perplexity は perplexity の自然対数では無いと言う事実を確認できるのが次の実験です。

前回の記事と同じく、4つのトピックにそれぞれ5単語を含む架空の言語で実験します。


import numpy as np
from gensim.corpora.dictionary import Dictionary
from gensim.models import LdaModel


word_list = [
    ["white", "black", "red", "green", "blue"],
    ["dog", "cat", "fish", "bird", "rabbit"],
    ["apple", "banana", "lemon", "orange", "melon"],
    ["Japan", "America", "China", "England", "France"],
]
corpus_list = [
    np.random.choice(word_list[topic], 100)
    for topic in range(len(word_list)) for i in range(100)
]

# 単語と単語IDを対応させる辞書の作成
dictionary = Dictionary(corpus_list)
# LdaModelが読み込めるBoW形式に変換
corpus = [dictionary.doc2bow(text) for text in corpus_list]

# トピック数4を指定して学習
lda = LdaModel(corpus, num_topics=4, id2word=dictionary)

# log_perplexity を出力
print(lda.log_perplexity(corpus))
# -2.173078593289852

出力が $-2.17\dots$です。
正常に学習できていれば、Perplexityは約5になるはずなので、$\log(5)=1.609\dots$が期待されるのに、符号から違います。

ドキュメントをよく読んでみましょう。
log_perplexity

Calculate and return per-word likelihood bound, using a chunk of documents as evaluation corpus.
Also output the calculated statistics, including the perplexity=2^(-bound), to log at INFO level.

これによると、perplexityは$2^{-bound}$だと言うことになっていて、どうやら、
log_perplexity()が返しているのは、boundに相当するようです。

計算してみましょう。


print(2**(-lda.log_perplexity(corpus)))
# 4.509847333880428

正解は5なので、それらしい結果が出ています。
ですがしかし、Perplexityとしてはこの値は良すぎます。
今回のダミーデータで学習している限りは5単語未満に絞り込めるはずがないのです。

実際、モデルが学習した結果を見てみましょう。


print(lda.show_topics(num_words=6))
"""
[
    (0, '0.100*"bird" + 0.098*"dog" + 0.092*"melon" + 0.092*"cat" + 0.089*"orange" + 0.089*"rabbit"'), 
    (1, '0.104*"red" + 0.104*"green" + 0.102*"white" + 0.098*"blue" + 0.092*"black" + 0.084*"fish"'),
    (2, '0.136*"lemon" + 0.134*"apple" + 0.128*"banana" + 0.117*"orange" + 0.116*"melon" + 0.045*"China"'),
    (3, '0.216*"France" + 0.191*"America" + 0.181*"Japan" + 0.172*"England" + 0.163*"China" + 0.011*"apple"')
]
"""

本来は各トピック上位の5単語を0.2ずつの出現確率と予測できていないといけないので、今 Perplexity を計算しているモデルは
そんなに精度が良くないのです。(ハイパーパラメーターのチューニングを何もしてないので。それはそれで問題ですが、今回の議題からは外します。)

おかしいので、ソースコードを眺めてみたのですが、 2を底とする対数を取ってる様子は無く、普通に自然対数が使われていました。
なので、これはドキュメントの誤りとみた方が良さそうです。(将来的に修正されると思います。)

perplexity=e^(-bound)

と考えると、辻褄があいます。


print(np.exp(-lda.log_perplexity(corpus)))
# 8.785288789149925

トピック数を 1〜4 と動かして算出してみると明らかです。
トピック数が1の時は全く絞り込めていないので元の単語数の約20,
2の時は半分に絞れるので約10,
4の時は、ちゃんと学習できたら正解の5(ただし、デフォルトのハイパーパラメーターだとそこまで成功しないのでもう少し大きい値)
が算出されるはずです。

やってみます。


for i in range(1, 7):
    # トピック数を指定してモデルを学習
    lda = LdaModel(corpus, num_topics=i, id2word=dictionary)

    print(f"トピック数: {i}, Perplexity: {np.exp(-lda.log_perplexity(corpus))}")
"""
トピック数: 1, Perplexity: 20.032145913774283
トピック数: 2, Perplexity: 11.33724134037765
トピック数: 3, Perplexity: 8.921203895821304
トピック数: 4, Perplexity: 7.436279264160588
トピック数: 5, Perplexity: 7.558708610631221
トピック数: 6, Perplexity: 5.892976661122544
"""

大体想定通りの結果は出ましたね。

さて、 log_perplexity は perplexity の対数では無く、
perplexity 対数の符号を反転させたものである、と言うのは認識しておかないと大間違いの元になります。

というのも、perplexityは小さい方が良いとされる指標です。
と考えると、log_perplexityの出力が小さいパラメーターを選んでしまいがちですがそれは誤りであることがわかります。
対数を取ってから符号を反転させているので、大きいものを採用しないといけないのですね。

(この他、必ずしもPerplexityが小さければいいモデルだとは言えない、と言う議論もあるのですが、
今日の記事の範囲は超えますので省略します。)

トピックモデルの評価指標Perplexityの実験

このブログでトピックモデルの記事を書いたことがあるのですが、
トピック数の決め方について書いてないのに気づいたので評価指標を紹介します。

参考: pythonでトピックモデル(LDA)

トピックモデルのトピック数を決めるときは、Perplexityもしくは、Coherenceと呼ばれる指標を参考にします。
今回の記事では、Perplexityを紹介します。

と言っても、数学的な定義やその意味についてはいろんな場所で紹介されているので、
この記事では趣向を変えて、架空のデータで実験して理解を深めることを目指します。

まず、 Perplexity の定義は、各単語の出現確率(尤度)の逆数の幾何平均です。
(数式はいろんなサイトに乗っているので省略します。
書籍では、奥村学さんの「トピックモデルによる統計的潜在意味解析」などに載っています。)

この定義だけでは意味がわからないのですが、
「分岐数、または選択肢の数を表している」と説明されることが多いです。

例えば、ある文章があって、単語が一つ隠されていたとします。
このとき、LDAによって、その単語の選択肢が2000まで絞り込めていたら、
そのモデルの Perplexity は 2000です。
単語を絞り込めている方が優れたモデルとされるので、この値は低い方が良いモデルです。

まだわかりにくいので、ここから実験をしていきましょう。
次のような架空の世界があったとします。

– その世界の言葉には4個の話題(トピック)がある。
– 各話題ごとに、単語は5個ある。(つまりその世界に単語は20個しか無い)
– 各文章は一つの話題のみに言及する。

(これらの条件は正確にはトピックモデルではなく、混合ユニグラムモデルですが、わかりやすさのためご容赦ください。)

以上の設定のもとで、ランダムに100単語からなる文章をトピックごとに100個生成します。
コードを見ていただけるとわかりますが、4個の話題は色、動物、果物、国です。(なんでも良いのですが。)


import numpy as np

word_list = [
    ["white", "black", "red", "green", "blue"],
    ["dog", "cat", "fish", "bird", "rabbit"],
    ["apple", "banana", "lemon", "orange", "melon"],
    ["Japan", "America", "China", "England", "France"],
]
corpus = [
    " ".join(np.random.choice(word_list[topic], 100))
    for topic in range(len(word_list)) for i in range(100)
]

さて、あとは以前紹介したコードで、LDAモデルを作って、Perplexityを計算してみましょう。
scikit-learnの場合、ドキュメントにある通り、モデルがperplexityというメソッドを持っています。

トピック数はこの例では4が正解だとわかっているので、4を使います。

本当は、データを訓練データと評価データにわけて、評価データでperplexityを計算する必要があるのですが、
今回は実験なので訓練に使ったデータでそのまま評価します。


from sklearn.feature_extraction.text import CountVectorizer
from sklearn.decomposition import LatentDirichletAllocation

tf_vectorizer = CountVectorizer()
bow = tf_vectorizer.fit_transform(corpus)

# LDAのモデル作成と学習
lda = LatentDirichletAllocation(
    n_components=4,
)
lda.fit(bow)
# perplexityの計算
print(lda.perplexity(bow))
# 5.268629755256359

Perplexity は約 5.27 と、 5に近い値が出ましたね。
このLDAモデルで単語が5個くらいまで絞り込めていることがわかります。

Perplexity がトピック数の決定に使えることをみるために、他のトピック数でも計算してみましょう。


for c_num in range(1, 9):
    lda = LatentDirichletAllocation(
        n_components=c_num,
    )
    lda.fit(bow)
    print(f"トピック数: {c_num}, Perplexity: {lda.perplexity(bow)}")
"""
トピック数: 1, Perplexity: 20.033955224623902
トピック数: 2, Perplexity: 10.330848184515682
トピック数: 3, Perplexity: 7.397066706843117
トピック数: 4, Perplexity: 5.268629755256354
トピック数: 5, Perplexity: 5.305381334487885
トピック数: 6, Perplexity: 5.3074106945229875
トピック数: 7, Perplexity: 5.3206895866734305
トピック数: 8, Perplexity: 5.3529382429024315
"""

トピック数が1個の時は、全く絞り込めていないので、全単語数の20に近い値が出ています。
トピック数が2の場合は、半分に絞れているので約10ですね。
そして、トピック数が4の時に、大体5単語に絞れており、
それ以上トピック数を増やしても大きな改善はありません。
このことから、トピック数は4がベストだろうと判断することができます。

現実世界のデータで試すと、こんなに綺麗にトピック数を決めれたことが無く、
Perplexity の有効性に疑問を持っていたのですが、
理論的にはなかなか良い指標であることが確認できました。

WordPressのテーマ Twenty Seventeen のフッターを変更する

これまで、せっかく子テーマを設定したりblogの開発環境を立ち上げる方法を調べたりしたので、1箇所くらい修正してみようと思います。
修正したいのは、ブログの一番下のフッター部分です。
デフォルトでは、
「プライバシーポリシー / Proudly powered by WordPress」
となっています。
プライバシーポリシーへのリンクはそのままでいいですが、 Proudly powered by WordPress は特にいらないですね。
代わりに入れたいのはコピーライト表記です。
また、プロフィールページ(これはそのうち作りたい)がまだないので、
その代わりとして、LinkedInあたりへのリンクを作りたいと思っています。

さて、WordPressのテーマを子テーマを用いて修正する場合、
子テーマのディレクトリの配下に、親テーマと同じ配置でファイルを作ると、それが上書きされて動作します。
参考: WordPressの子テーマのページ

つまり、
(略)/themes/twentyseventeen-child/footer.php
と言うファイルを作ると、このファイルが、
(略)/themes/twentyseventeen/footer.php
を上書きして動作します。

現在利用している Twenty Seventeen の場合、 footer.phpの作りが少し特殊です。

get_template_part( 'template-parts/footer/site', 'info' );

とある通り、
(略)/themes/twentyseventeen-child/template-parts/footer/site.php
というファイルを読み込んでそれを表示しています。

開いてみると以下の内容でした。確かにこれがフッターの本体のようですね。


<?php
/**
 * Displays footer site info
 *
 * @package WordPress
 * @subpackage Twenty_Seventeen
 * @since Twenty Seventeen 1.0
 * @version 1.0
 */

?>
<div class="site-info">
        <?php
        if ( function_exists( 'the_privacy_policy_link' ) ) {
                the_privacy_policy_link( '', '<span role="separator" aria-hidden="true"></span>' );
        }
        ?>
        <a href="<?php echo esc_url( __( 'https://wordpress.org/', 'twentyseventeen' ) ); ?>" class="imprint">
                <?php
                        /* translators: %s: WordPress */
                printf( __( 'Proudly powered by %s', 'twentyseventeen' ), 'WordPress' );
                ?>
        </a>
</div><!-- .site-info -->

これを上書きするように子テーマにファイルを作成します。

なのでファイルの置き場所は以下になります。
(略)/themes/twentyseventeen-child/template-parts/footer/site-info.php

中身はこんな感じで作ってみました。
(blogのURLと名前は、bloginfo(‘url’)とbloginfo(‘name’)でそれぞれ取得できるみたいです。)


<?php
/**
 * Displays footer site info
 *
 * @package WordPress
 * @subpackage Twenty_Seventeen
 * @since Twenty Seventeen 1.0
 * @version 1.0
 */

?>
<div class="site-info">
    <?php
        if(function_exists('the_privacy_policy_link')){
            the_privacy_policy_link('', '<span role="separator" aria-hidden="true"></span>');
    }
    ?>
    <a href="https://www.linkedin.com/in/yutaro-honda/">LinkedIn</a>
    <span role="separator" aria-hidden="true"></span>
    <a href="<?php bloginfo('url');?>">
        &copy; 2019 <?php bloginfo('name');?>
    </a>
</div><!-- .site-info -->

これでフッターを編集できました。

pandasのDataFrameをある列の値が特定の区間に含まれる行のみに絞る

pandasのSeriesにbetweenという便利なメソッドが定義されていたのでその紹介です。
これを使わなくても何も難しいことのない話なのですが、コードが少し短くなってすっきりするので気に入っています。

ドキュメント: pandas.Series.between

これは、Seriesに対して、最小値、最大値を渡すと、
Seriesの各値に対して、その値が最小値と最大値の範囲に入っていればTrue, 入っていなければFalseを返すメソッドです。
これを使って、DataFrameの列の絞り込みができます。

試すために、いつものirisでDataFrameを作っておきます。


import pandas as pd
from sklearn.datasets import load_iris
# アヤメのデータを読み込んでDataFrameに整形
iris = load_iris()
columns = [c.replace(" (cm)", "") for c in iris.feature_names]
df = pd.DataFrame(iris.data, columns=columns)
df["target"] = [iris.target_names[t] for t in iris.target]
print(df.head())
"""
   sepal length  sepal width  petal length  petal width  target
0           5.1          3.5           1.4          0.2  setosa
1           4.9          3.0           1.4          0.2  setosa
2           4.7          3.2           1.3          0.2  setosa
3           4.6          3.1           1.5          0.2  setosa
4           5.0          3.6           1.4          0.2  setosa
"""

このようなデータから、たとえば、”petal length” が 4.0以上4.5以下の行を抽出したいとします。
それを単純に書くとこうなります。


df[(df["petal length"] >= 4.0) & (df["petal length"] <= 4.5)]

DataFrameの変数名を3回も書かないといけないですし、ちょっと冗長ですね。

これが betweenを使うと、次のように書けます。


df[df["petal length"].between(4.0, 4.5)]

少しだけすっきりしました。

実際に絞り込めているのを確認しておきましょう。 describe()メソッドを使って、その中から最小値と最大値だけ取ってみます。


print(df[df["petal length"].between(4.0, 4.5)].describe().loc[["min", "max"]])
"""
     sepal length  sepal width  petal length  petal width
min           4.9          2.2           4.0          1.0
max           6.7          3.4           4.5          1.7
"""

注意ないといけないのは、選択されるのは、
1つ目の引数 $<=$ 指定列の値 $<=$ 2つ目の引数 と言うふうに左右両方とも等号が入った閉区間の値であることです。 3つ目の引数、 inclusive にFalse を指定すると等号を含まなくなるのですが、これは最大最小両方とも統合を含まず、 1つ目の引数 $<$ 指定列の値 $<$ 2つ目の引数 の区間を取得するようになります。


print(df[df["petal length"].between(4.0, 4.5, False)].describe().loc[["min", "max"]])
"""
     sepal length  sepal width  petal length  petal width
min           5.5          2.3           4.1          1.0
max           6.7          3.1           4.4          1.5
"""

一番頻繁に使う、x以上、y未満、と言う形式の指定ができないのが短所ですね。

以上未満で指定したい場合は、下記のようにqueryメソッドなど別の方法も検討しましょう。


df.query("4.0 <= `petal length` < 4.5")

Googleアナリティクスの集計対象をホスト名でフィルタリングする

先日の記事で、WordPress(Lightsail)の開発環境を立てましたが、一つ問題があります。
そのページを開くとGoogleアナリティクスにアクセスログが記録されることです。

参考: LightsailのWordPressの開発環境を立てる

そんなにたくさんアクセスしたわけではないのでほとんど問題ないのですが、
分析する上でノイズになりうるのでできるだけ除外しておきたいです。

対策としては、開発環境を立ち上げたらすぐに管理画面に入ってGA関係の設定を外すこともできますし、
自宅のIPアドレスをフィルタリングしておくことも考えられます。
(そもそも、後者ができていれば今回の記事の対策は不要です。)

これらの方法以外にも、アクセスするときのホスト名によって集計対象とするかどうかを設定できるので、今回はそれを設定します。
要するに、
https://analytics-note.xyz/foo/bar/
と言うアクセスだけを集計し、
https://{IPアドレス}/foo/bar/
などのアクセスを集計対象から外します。

方法は非常に簡単です。

1. Googleアナリティクスの設定画面を開く。
2. 「フィルタ」を選択。
3. 「+フィルタを追加」を選択
4. 以下の内容を入力
フィルタ名 ・・・ 任意
フィルタの種類 ・・・ 定義済み、 右のみを含む、 ホスト名へのトラフィック、 等しい
ホスト名 ・・・ analytics-note.xyz
5. 「このフィルタを確認する」 を押下して設定を確認する。
6. 「保存」

これで、開発用に立ち上げた環境のアクセスログは集計されなくなりました。