Googleアナリティクスで、特定のページを訪れたセッションの数を見たい時の注意点

前の記事で、Googleアナリティクスのセッションの定義を紹介しました。
このブログはセッションあたりのpvが1.2くらいしかないのであまり気になりませんが、業務でサイトの分析をするときなど、
各ページを訪問したセッション数を知りたいことがあります。

例えば、
ページA -> ページB -> ページC
と遷移したユーザーと、
ページB -> ページD -> ページB
と遷移したユーザーがいた時に、ページBを訪問したセッションが2個だ、と数えたいケースです。

これを知るには、「ページ別訪問数」という指標をみる必要があります。
「セッション」ではないので、非常に間違えやすく、注意が必要です。

ちなみに「セッション」は、各ページでセッションが始まった時だけカウントされます。
上の例で言えばページAとページBはセッションが1です。

ヘルプではこの辺りを読むと参考になります。
アナリティクスの Google 広告クリック数、セッション数、ユーザー数、閲覧開始数、ページビュー数、ページ別訪問数の違い

もともと用意されている各レポートでは適切に使い分けられて表示されているのですが、
カスタムレポートなどを使うときは特に注意したいですね。

Googleアナリティクスにおけるセッションの数え方

セッションというのはWebサービスに対する一連のアクセスのことです。
ある人がこのサイトを訪れ、3ページ見て離脱していったらその3pvが1セッションとなります。

ただ、pvのような数え方が明確なものと違って、セッションを数えるには何を基準に離脱していったかを定義する必要があります。
Googleアナリティクスにおいてもセッション数は取得されているのですが、
それを正しく使うにはGoogleアナリティクスにおけるセッションの定義を知っておく必要があります。

その内容ですが、アナリティクスのヘルプの下記ページに書かれています。
アナリティクスでのウェブ セッションの算出方法

ようは次の3つの条件のどれかを満たすと、そのセッションは終わりと判断され、新しいセッションに移ります。

  • 30分間操作がない
  • 午前0時をまたぐ
  • 他サイトを経由してアクセスし直した

普段自分でクエリを書いてセッションを区別するときはユーザーのアクセスが30分途切れたら別セッション、
という条件だけで済ませていることが多いので2番目と3番目の条件をよく忘れてしまいます。
(そんなのは自分だけだと思いますが。)

Kerasの学習履歴(History)をDataFrameに変換する

Kerasのちょっとした小ネタです。
Kerasで作ったモデルをfitすると、戻り値として損失関数や正解率を格納したHistoryオブジェクトが返されます。
それを使って、学習の進みなどを可視化できます。

例えばこちらの記事を参照: CNNで手書き数字文字の分類
こちらでは可視化だけで10行くらいのコードになっています。

で、改めてHistoryの中身をみてみると、DataFrameに変換できる形式であることに気づきました。
長いので、実データは {数値のリスト} に置換しましたが、次のようなイメージです。


print(history.history)
'''
{'val_loss': {数値のリスト},
 'val_acc': {数値のリスト},
 'loss': {数値のリスト},
 'acc': {数値のリスト}
'''

これは容易にDataFrameに変換できます。


print(pd.DataFrame(history.history))
'''
    val_loss  val_acc      loss       acc
0   0.106729   0.9677  0.590888  0.811850
1   0.072338   0.9764  0.227665  0.931233
2   0.059273   0.9800  0.174741  0.948033
3   0.047335   0.9837  0.149136  0.955500
4   0.042737   0.9859  0.132351  0.960167
5   0.039058   0.9868  0.121810  0.964600
6   0.034511   0.9881  0.110556  0.967050
7   0.032818   0.9882  0.105487  0.967867
8   0.032139   0.9893  0.100333  0.970167
9   0.030482   0.9898  0.095932  0.971383
10  0.027904   0.9900  0.089120  0.973267
11  0.028368   0.9898  0.086760  0.973683
'''

DataFrameになると、これ自体を分析しやすいですし、さらに非常に容易に可視化できます。


history_df = pd.DataFrame(history.history)
history_df.plot()
plt.show()

たったこれだけです。以前の記事の可視化に比べると非常に楽ですね。
出力はこのようになります。

INSERT文でWITH句を使う

PrestoのINSERT文で、別のテーブルからSELECTした結果を挿入する書き方があります。
こういうの。


INSERT INTO
    new_table (
        col1,
        col2,
        col3
    )
SELECT
   col1,
   col2,
   col3
FROM
   old_table

このとき、SELECT文がそこそこ複雑になると、以前の記事で紹介したWITH句を使いたくなるのですが、うまく動かず困っていました。
それはどうやら、 INSERT INTO より先に WITH句を書いてしまっていたのが原因のようです。

WITH は INSERT INTO と SELECT の間に 書くのが正解のようです。
例としてはこんな感じ。


INSERT INTO
    new_table (
        col1,
        col2,
        col3
    )
WITH
    tmp_table AS (
        SELECT
            col1,
            col2,
            col3
        FROM
            old_table
    )
SELECT
   col1,
   col2,
   col3
FROM
   tmp_table

scikit-learnでK-分割交差検証

普段、機械学習のパラメーターを最適化するために
K分割交差検証をするときはGridSearchCVで済ましているのですが、
今回別の目的があって、K-分割交差検証のような分割が必要になりました。
参考:scikit-learn でグリッドサーチ

そこで、GridSearchCVの中でも使われているKFoldを使ってみたのでその記録です。
ドキュメントはこちら。
sklearn.model_selection.KFold

注意点は、splitした時に得られる結果が、データそのものではなくデータのインデックスである点くらいです。

実行してみるために、まずダミーデータを準備します。


from sklearn.model_selection import KFold
import numpy as np

# ダミーデータの準備
X = np.random.randint(20, size=(10, 2))
y = np.random.choice([0, 1], size=10)
print(X)
'''
[[ 2 12]
 [ 6  3]
 [ 0  3]
 [15  9]
 [19  0]
 [12 14]
 [ 0  0]
 [12 18]
 [ 1  9]
 [ 3 14]]
'''
print(y)
# [0 0 0 1 1 1 0 1 0 0]

次に、 KFoldを動かしてみましょう。
まず挙動を確認したいので、splitして得られるイテレータの中身を表示してみます。


kf = KFold(5, shuffle=True)
for train_index, test_index in kf.split(X):
    print("train_index:", train_index)
    print("test_index:", test_index)
    print()  # 1行開ける

# 以下出力
'''
train_index: [1 2 3 4 5 6 8 9]
test_index: [0 7]

train_index: [0 1 2 3 4 6 7 9]
test_index: [5 8]

train_index: [0 1 3 4 5 6 7 8]
test_index: [2 9]

train_index: [0 1 2 5 6 7 8 9]
test_index: [3 4]

train_index: [0 2 3 4 5 7 8 9]
test_index: [1 6]

'''

ご覧の通り、0〜9の値を、5つのグループに分けて、順番に一個をテスト用にしながら交差検証用のデータセットのインデックスを返してくれています。
インデックスを元にデータを分割するには、つぎのようにして別の変数に取り出すと以降のコードが読みやすくなります。


X_train, X_test = X[train_index], X[test_index]
y_train, y_test = y[train_index], y[test_index]

pandasでユニコード正規化

3記事続けてのユニコード正規化の話です。
これまで標準ライブラリのunicodedata.normalizeを使っていましたが、
実はpandasのDataFrameやSeriesにもユニコード正規化のメソッドが実装されています。

ドキュメント: pandas.Series.str.normalize

これを使うと大量の文字列を一気に正規化できるので、個人的にはこちらを使うことが多いです。
機械学習で、学習時はpandasのnormalizeを使い、
その後、個々のデータを予測する時にunicodedata.normalizeを使ってしまうと、結果変わってしまう恐れがあるのではないかと
心配して調べたことがあるのですが、pandasのnormalizeはunicodedataのラッパーになっていて、
中では同じモジュールを使っているので問題ありませんでした。
(ドキュメントを読んでもわかりますね。)

pandas の v0.25.0 のコードから抜粋しますが、この通り、unicodedata.normalizeを呼び出しているだけです。


    @forbid_nonstring_types(["bytes"])
    def normalize(self, form):
        """
        Return the Unicode normal form for the strings in the Series/Index.
        For more information on the forms, see the
        :func:`unicodedata.normalize`.
        Parameters
        ----------
        form : {'NFC', 'NFKC', 'NFD', 'NFKD'}
            Unicode form
        Returns
        -------
        normalized : Series/Index of objects
        """
        import unicodedata

        f = lambda x: unicodedata.normalize(form, x)
        result = _na_map(f, self._parent)
        return self._wrap_result(result)

実際に使うと、次のようになります。
Seriesを例にとりましたが、DataFrameの列を対象にする場合も同様です。


import pandas as pd
series = pd.Series(
    [
        "パピプペポ",
        "①⑵⒊",
        "㍾㍽㍼㍻",
        "㌢ ㌔ ㍍"
    ]
)
print(series.str.normalize("NFKC"))

# 以下出力
'''
0          パピプペポ
1         1(2)3.
2       明治大正昭和平成
3    センチ キロ メートル
dtype: object
'''

正規化形式別のユニコード正規化の振る舞いの違いを見てみる

前回の記事でユニコード正規化を紹介し、NFD/NFC/NFKD/NFKCの4種類の形式があるという話をしました。
今回はそれぞれの形式で正規化した時の振る舞いを見ていこうと思います。

元々、各形式の厳密な定義の話をしようと結構前から調べていたのですが、
正準等価の方(NFDとNFC)がまだ自分の中で腑に落ちてないので、今回は色々動かして結果を眺めることにします。
互換等価の方は、意味は同じで見た目が違う文字、というざっくりとした理解で大丈夫のようです。(たぶん)

ちなみに、4つの中でどれを使えば良いか迷ってるだけ、という人は NFKC を使えば大丈夫だと思います。
それでは、どの型で正規化されるかによって結果が変わる文字をいくつか取り上げて挙動を見てみましょう。

一つ目はひらがなの「が」です。
NFC/NFKCでは特に変化がなく、 NFD/NFKDでも、見た目は変化してないのですが、文字コードに直すと「か」と「濁点」に分解されていることがわかります。


import unicodedata
forms = ["NFC", "NFD", "NFKC",  "NFKD"]

text = "が"
print("原型:", text, "  文字コード:", text.encode("utf-8"))
for form in forms:
    print(
        form,
        ":",
        unicodedata.normalize(form, text),
        "  文字コード:",
        unicodedata.normalize(form, text).encode("utf-8")
    )
    
# 以下出力
'''
原型: が   文字コード: b'\xe3\x81\x8c'
NFC : が   文字コード: b'\xe3\x81\x8c'
NFD : が   文字コード: b'\xe3\x81\x8b\xe3\x82\x99'
NFKC : が   文字コード: b'\xe3\x81\x8c'
NFKD : が   文字コード: b'\xe3\x81\x8b\xe3\x82\x99'
'''

つぎは半角カタカナの「カ」です。
NFKC/NFKDでは全角のカタカナに正規化してくれていることがわかります。
NFC/NFDは変化なしです。


text = "カ"
print("原型:", text, "  文字コード:", text.encode("utf-8"))
for form in forms:
    print(
        form,
        ":",
        unicodedata.normalize(form, text),
        "  文字コード:",
        unicodedata.normalize(form, text).encode("utf-8")
    )

# 以下出力
'''
原型: カ   文字コード: b'\xef\xbd\xb6'
NFC : カ   文字コード: b'\xef\xbd\xb6'
NFD : カ   文字コード: b'\xef\xbd\xb6'
NFKC : カ   文字コード: b'\xe3\x82\xab'
NFKD : カ   文字コード: b'\xe3\x82\xab'
'''

次は、「ガ」です。
NFC/NFDは変化しないのは「カ」の時と同じですが、
NFKCとNFKDで、文字コードが違います。NFKDの方は「カ」と「濁点」に分解されたままですが、
NHKCではそれが結合されています。


text = "ガ"
print("原型:", text, "  文字コード:", text.encode("utf-8"))
for form in forms:
    print(
        form,
        ":",
        unicodedata.normalize(form, text),
        "  文字コード:",
        unicodedata.normalize(form, text).encode("utf-8")
    )

# 以下出力
'''
原型: ガ   文字コード: b'\xef\xbd\xb6\xef\xbe\x9e'
NFC : ガ   文字コード: b'\xef\xbd\xb6\xef\xbe\x9e'
NFD : ガ   文字コード: b'\xef\xbd\xb6\xef\xbe\x9e'
NFKC : ガ   文字コード: b'\xe3\x82\xac'
NFKD : ガ   文字コード: b'\xe3\x82\xab\xe3\x82\x99'
'''

互換等価性は正準等価性より広い概念で、正準等価であるものは何であれ互換等価とのこと(参考:wikipedia – Unicodeの等価性)
なので、NFKCやNFKDが元の文字列と同じで、NFCやNFDは元の文字列と異なる、という例はおそらく無いのでしょう。

このほか特殊記号など色々試してみましたが、全体的に NFKC が僕が欲しい結果になることが多かったので、
普段はこれを利用しています。

Pythonでユニコード正規化

テキストデータを機械学習にかける時などによく行われる処理に、ユニコード正規化(Unicode normalization)と呼ばれるものがあります。
要するに同じ意味(等価)な文字や文字列の内部表記を統一する一種の名寄せのようなものです。
なお、正規化にはNFD/NFC/NFKD/NFKCの4種類があります。
Unicode正規化 出典: フリー百科事典『ウィキペディア(Wikipedia)』

日本語で使われる等価な文字の例をあげると、 アとア、①と1 ㍼と昭和、㌕と、キログラム、などがあります。

Pythonには unicodedata という標準ライブラリが用意されており、
手軽にユニコード正規化を行えます。

参考:unicodedata — Unicode データベース

使うのはunicodedata.normalize("正規化形式", "正規化したい文字列") という関数です。

上に上げていた例でやってみるとこのような感じになります。 


import unicodedata
print(unicodedata.normalize("NFKC", "ア ① ㍼ ㌕"))
# ア 1 昭和 キログラム

ちなみに “㋿” (令和) には対応していませんでした。
僕の環境のライブラリが古いのかもしれません。
(そもそも人によってはブラウザでも正しく表示されていないかもしれません。)

PrestoのWITH句

2年ほど前、Prestoのクエリを書き始めた時に知って感動して以来、
ずっと使い続けている文法に WITH句 があります。

イメージとしてはSQL中でVIEWのようなものを作れる構文で、
これを使うとクエリのネストを浅くするなど可読性を大幅に向上させることができます。

例えば次の2つのクエリは同じ結果を返してくれます。
(この例だとまだメリットがわかりにくいですね。そもそも一番外側のSELECT句が純粋に無駄)
伝えたいのはSELECT句のネストがなくなり、フラットな書き方ができるということです。
階層がより深かったり、数百行以上の規模のクエリになるとこの効果は劇的に大きく出ます。
各一時テーブルに名前をつけられることも意図を伝える上で非常に有益です。

通常のネスとした書き方。


SELECT
    col1,
    col2_max
FROM (
    SELECT
        col1,
        MAX(col2) AS col2_max
    FROM
        table1
    GROUP BY
        col1
);

WITH句を使った書き方。


WITH
    table2 AS (
        SELECT
            col1,
            MAX(col2) AS col2_max
        FROM
            table1
        GROUP BY
            col1
    )
SELECT
    col1,
    col2_max
FROM
    table2;

うちのチームでは(その人がSQLを十分書ける人であれば)
トレジャーデータのアカウントを渡して基本的な注意事項を説明したら、
すぐに WITH の使い方を教えています。

その時に使える公式なドキュメントとか無いのかな、と思って探していたのですが、
9.32. SELECT の WITH Clause がそれのようです。
(目次でWITHを検索しても出てこないのでこれまで見つけれてなかった。)
非常にあっさりとしか説明されてなくて、職場の既存クエリ読んでもらった方が良さそうな情報量ではあります。

numpyでビンを作成する

以前の記事で、pandas.cutを使ってデータをビンに区切る方法を紹介しました。
参考:pandasで数値データを区間ごとに区切って数える

これはこれで便利なのですが、似たようなことを行う関数がnumpyにも実装されていたのでその紹介です。
個人的にはこちらの方が好きです。
numpy.digitize

引数は次の3つをとります。
x : 元のデータ
bins : 区切り位置のリスト (1次元のリストで単調増加か単調減少のどちらかであることが必須)
right : 統合をどちらの端に含むか。(binsが単調増加か減少かも関係するのでドキュメントの説明を見ていただくのが確実です)

これを使うと、xの各データが、binsで区切られたなんばんめの区画に含まれるのかのリストを返してくれます。
binsは配列で渡すので等間隔でなくても使えます。

動かしてみたのがこちら。


import numpy as np
x = np.random.randint(200, size=10) - 100
print(x)
# [ 20  77  23 -50 -18 -80 -17  45  66  83]
print(np.digitize(x, bins=[-50, -10, 0, 10, 50]))
# [4 5 4 1 1 0 1 4 5 5]

bins に 5つの要素があるので、両端も含めて6つのbin(0〜5)にデータが区切られます。
例えば最初の20は、10<=20<50 なので、4番目の区画ですね。
right を省略し、左側に統合がついているので、
-50<=-50<-10 となり、-50は1番目の区画に入るということも確認できます。

Interval オブジェクトではなく、ただの数列で値を返してくれるのもありがたい。
(Intervalオブジェクトも便利なのかもしれませんがまだ慣れない。)