静かなる名辞

pythonとプログラミングのこと


list, tuple, dict, setの基本的な使い方、相違点、使い分け、応用など

はじめに

 pythonでよく使う組み込みのコレクション型には、list, tuple, dict, setなどがあります。ただ、なんとなく使っている、いつもlist、それぞれ微妙に使い勝手が違って困る、という人も多いと思います。そこで、これらについて解説します。

目次

コレクション型ってなに?

 とりあえず、「コレクション型」という用語は公式に使われているものではなく、便宜的にそう読んでいるだけなので注意してください。「中にオブジェクトを格納できる型(クラス)」程度の意味です。

 ドキュメントによれば、pythonの主要な組み込み型には以下のような区別があります。

主要な組み込み型は、数値、シーケンス、マッピング、クラス、インスタンス、および例外です。

組み込み型 — Python 3.7.4 ドキュメント

(なお、上のページはつらつらと読むとそれだけで勉強になります。この記事を読んで物足りないと思った人におすすめです。)

 さて、タイトルに含めた4つのうち、listとtupleはシーケンス、dictはマッピングです。setはどれにも当てはまりません。

 シーケンス、マッピングという言葉の意味は、なんとなくわかる人も多いと思います。シーケンスは順番があって連続的に、あるいはインデックスを指定したりしてアクセス可能なもの、マッピングはシーケンスと異なって順番を持ちませんが、dictのように任意のキーと値の対応付けを記録可能なものです(というか、基本的にdictとその亜種しかないと思います)。

 こういう「コレクション型」はpythonでプログラミングをする上ではとても重要なので、とりあえず組み込みの主だったものはすべて覚えておきましょう。どんなコードでも何らかの形で利用していたりするので、知らないと困ります。

listについて

基本

 コレクション型の中でもっとも代表的なのはlist(リスト)ですね。色々な場面で汎用的に使われます。初心者の方はとりあえずlistを覚えて、慣れないうちはなんでもlistで書くと良いと思います。

 リテラル*1で書く場合はこんな感じです。

>>> print(lst1)  # 空のリスト
[]
>>> lst2 = [1, 2, 3]
>>> print(lst2)
[1, 2, 3]

 要素を末尾に追加したければ、appendが使えます。

>>> lst1.append(4)
>>> lst2.append(5)
>>> print(lst1)
[4]
>>> print(lst2)
[1, 2, 3, 5]

 逆にlist以外でappendが使えるものはあまり(というか恐らくまったく)ありません。ついでに書いておくと、numpyやpandasにもappendがありますが、あれはあれでまた違った挙動になります。

 「拡張」というか、listを含む他のコレクション型など*2の中身をまとめて突っ込みたいときは、extendというメソッドが使えます。

>>> lst1.extend(lst2)
>>> print(lst1)
[4, 1, 2, 3, 5]

 あとは普通にインデックスで要素を取得したり、for文に渡してループしたりもできます。

>>> print(lst1[0])
4
>>> for x in lst1:
...     print(x)
... 
4
1
2
3
5

 インデックスで要素を削除したい場合はdel文で行えます。また、pop(こちらもインデックスで指定し、除去した要素を返す), remove(こちらはオブジェクトの値が一致するものを除去)などで取り除くこともできます。

>>> del lst1[3]
>>> print(lst1)
[4, 1, 2, 5]

 ただ、あまり使っているのを見ませんね。これをやるとインデックスが狂う上、ループと組み合わせると面倒くさい問題もあります。

【python】listをforループで回してremoveしたら思い通りにならない - 静かなる名辞


 append以外でlistの長さが減るような処理は積極的にやらない、という傾向が強いのではないでしょうか。

 listは「ミュータブルなシーケンス」という分類になり、実はとてもたくさんの操作が行えます。すべてはカバーできないので、ここ↓を見てください。

組み込み型 — Python 3.7.4 ドキュメント | シーケンス型 --- list, tuple, range

 繰り返しになりますが、これを読むととても勉強になります。

内包表記

 listといえばリスト内包表記というくらいよく使われるものです。

>>> print([x + 100 for x in range(5)])
[100, 101, 102, 103, 104]

 難しいものではないので、慣れましょう。

tupleについて

基本

 tuple(タプル)はlistの中身を一切変更できなくしたような使い勝手です。

>>> tup1 = ()  # 空のtuple
>>> tup2 = (1, 2, 3)
>>> print(tup1)
()
>>> print(tup2)
(1, 2, 3)

 注意しないといけないのは、外側の丸括弧()はtupleの本質ではないという点です。カンマがあればtupleは作れます。

>>> tup2 = 1, 2, 3
>>> tup2
(1, 2, 3)

 え、カンマなんかあちこちで使うじゃん(関数の引数リストとか)と思うと思いますが、原則的には「他の構文とみなせないときはtupleとみなす」というルールです。なんかややこしいですね。

 では丸括弧は何か? というと、数式で(a + b) * 2とかやるときの丸括弧と同じで、評価の順番を決めているだけです。これがあることで、関数の引数リストなどの他の構文と混同されないようにtupleを表すことができます。

 このことを知らないと、要素数1つのtupleを作りたいとき、こういうものを書いてしまいます。

>>> this_is_not_tuple = (4)
>>> print(type(this_is_not_tuple), this_is_not_tuple)
<class 'int'> 4

 要素数1のtupleは、下のようにしてください。

>>> this_is_tuple = (4, )
>>> print(type(this_is_tuple), this_is_tuple)
<class 'tuple'> (4,)

応用

 中身の変更できないtupleなんか何に使えるんだと思われるかもしれませんが、tupleのようなイミュータブル(変更不可能)なオブジェクトはhashableという大切な性質を持っています。後述のdictやsetのキー、要素にしたいとき、データをtupleで表現するということがよく行われます。

 tupleには内包表記はありません。内包表記的なことをしたい場合は、ジェネレータ式と組み合わせて以下のように書くと手っ取り早いでしょう。

>>> tuple(x - 100 for x in range(5))
(-100, -99, -98, -97, -96)

dict

基本

 dict(ディクトと呼びますが、辞書という訳の方が一般的です)はキーと値の対応付けを保持します。

 リテラルではこう書きます。

>>> dct1 = {}  # 空のdict
>>> dct2 = {"hoge":0, "fuga":1}
>>> print(type(dct1), dct1)
<class 'dict'> {}
>>> print(dct2)
{'hoge': 0, 'fuga': 1}

 {キー:値, ...}という感じの構文です。簡単ですね。

 値の追加には普通は代入を使います。また、累積代入文なども使えます。

>>> dct2["piyo"] = 2
>>> print(dct2)
{'hoge': 0, 'fuga': 1, 'piyo': 2}
>>> dct2["piyo"] += 999
>>> print(dct2)
{'hoge': 0, 'fuga': 1, 'piyo': 1001}

 削除はdel文でいいのですが、あまり使わないでしょう。

 直接for文などに渡すと、キーだけが出てきます。

>>> for x in dct2:
...     print(x)
... 
hoge
fuga
piyo

 keys, values, itemsというメソッドでそれぞれキー、値、キーと値のペアを取り出せます。必要に応じて活用してください。

>>> for k in dct2.keys():
...     print(k)
... 
hoge
fuga
piyo
>>> for v in dct2.values():
...     print(v)
... 
0
1
1001
>>> for k, v in dct2.items():
...     print(k, v)
... 
hoge 0
fuga 1
piyo 1001


組み込み型 — Python 3.7.4 ドキュメント

重要な性質

 dictにはいくつか覚えておかないといけない性質があります。

 まず、tupleのところでも述べたとおり、キーにはimmutableなオブジェクトしか使えません。

>>> dct3 = {[1, 2]:3}
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unhashable type: 'list'
>>> dct3 = {(1, 2):3}
>>> print(dct3)
{(1, 2): 3}

 また、基本的には順番の概念はない、ということも知っておく必要があります。伝統的なPythonでは、辞書のキー・値の順番は適当にシャッフルされていました(内部的には意味があったのですが、割愛)。

# Python 3.5で実行
>>> d = {"hoge":123, "fuga":456, "piyo":789}
>>> print(d)
{'fuga': 456, 'hoge': 123, 'piyo': 789}

 最近のPython*3では順番を保持しますが、それでもappendのような順序を前提としたオペレーションは弱いので、扱いは以前とさほど変わりません。

# Python 3.6で実行
>>> d = {"hoge":123, "fuga":456, "piyo":789}
>>> print(d)
{'hoge': 123, 'fuga': 456, 'piyo': 789}

 まだあります。上で書いたkeys, values, itemsはとても便利ですが、これらは辞書ビューオブジェクトという集合っぽいオブジェクトで、しかもリアルタイムで辞書の中身を反映してくれます。ただし、ループ中にいじったりすると意図しない結果になったり、エラーが出たりします。

ループで辞書の要素を削除しようと思ったらRuntimeError: dictionary changed size during iteration - 静かなる名辞

 便利ですが、扱いには少し気をつけた方が良いのが辞書です。

内包表記

 dictにも内包表記があります。書き方はリスト内包表記と辞書のリテラルを混ぜた感じです。

>>> dct4 = {i:i**2 for i in range(5)}
>>> print(dct4)
{0: 0, 1: 1, 2: 4, 3: 9, 4: 16}

 まあ、これといって難しいことはないかと。

眷属たち

 dictには覚えておくと便利な標準の亜種がいくつかあります。collectionsモジュールから使えます。

 筆頭はdefaultdictでしょう。これはキーの初期値を適当に設定できるもので、データの集計などで威力を発揮します。

# 長さの異なる文字列ごとにグルーピングする処理
>>> lst = ["a", "b", "ab", "abc", "bcd"]
>>> d = {}  # defaultdictを使わない場合
>>> for x in lst:
...     l = len(x)
...     if l in d:
...         d[l].append(x)
...     else:
...         d[l] = [x]
... 
>>> print(d)
{1: ['a', 'b'], 2: ['ab'], 3: ['abc', 'bcd']}
# defaultdictを使う場合
>>> from collections import defaultdict
>>> d = defaultdict(list)
>>> for x in lst:
...     l = len(x)
...     d[l].append(x)    # d[l]がなければ勝手に空のリストを初期値にしてくれる
... 
>>> print(d)
defaultdict(<class 'list'>, {1: ['a', 'b'], 2: ['ab'], 3: ['abc', 'bcd']})

 他の使い方はdictとほぼ同じです。

 また、Counterは引数の頻度を勝手に集計してくれます。これも辞書の眷属みたいなものです。

>>> from collections import Counter
>>> lst = [1, 1, 2, 3, 4, 3, 2, 5]
>>> cnt = Counter(lst)
>>> print(cnt)
Counter({1: 2, 2: 2, 3: 2, 4: 1, 5: 1})
>>> print(cnt[0])
0
>>> print(cnt[1])
2

 このあたりを知っているかどうかでコードのクオリティが変わるので、覚えておきましょう。
collections --- コンテナデータ型 — Python 3.7.4 ドキュメント

set

基本

 トリを飾るのはsetです(セットとも呼びますが、表記するときは集合型とするかsetとそのまま書くかのどちらかです)。

 setは数学的な集合を表現できます。あるいは、dictをkeyだけにしたようなものと考えてもいいでしょう(内部ではどちらもハッシュテーブルを使っています)。

 リテラルで書くとき注意しないといけないのは、空のsetをリテラルで表現する手段が基本的にないということです。コンストラクタを使ってset()で作ります(これまで触れていませんでしたが、list(), tuple(), dict()でそれぞれ空のコレクションを作れます)。

>>> s1 = set()
>>> print(s1)
set()
>>> s2 = {1, 2, 3}
>>> print(s2)
{1, 2, 3}

 集合なので、要素は重複しません。強制的に1つにまとめられます。これを利用して、list→setに変換してユニークな要素の数を数えるというテクニックもよく使われます。

>>> s3 = {1, 2, 3, 3}
>>> print(s3)
{1, 2, 3}
>>> lst = [1, 2, 3, 3]
>>> print(len(set(lst)))
3

 集合なので、様々な演算が行なえます。

>>> s1 = {1, 2, 3, 4}
>>> s2 = {3, 4, 5, 6}
>>> print(s1 & s2)  # AND
{3, 4}
>>> print(s1 | s2)  # OR
{1, 2, 3, 4, 5, 6}
>>> print(s1 - s2)  # 引き算
{1, 2}
>>> print(s2 - s1)
{5, 6}
>>> print(s1 ^ s2)  # 排他的論理和
{1, 2, 5, 6}

 集合型を使う場面というのはこういう演算を使うことを期待している場合で、逆にそれ以外だと上のように「重複を除く」などで使う可能性がある以外には、あまり使いみちはありません。

 要素の追加はaddメソッドを使います。取り除くときはremoveかdiscardを使うことになるでしょう(存在しない要素に対して行った場合にエラーになるかどうかだけが異なります)。listの操作とはまったく互換性がないので、注意してください。appendは使えません。

>>> s = set()
>>> s.add(1)
>>> print(s)
{1}
>>> s.discard(1)
>>> print(s)
set()

組み込み型 — Python 3.7.4 ドキュメント

 dictのキーと同じく、中に入れられるのはimmutable(厳密にはhashableであればよいが)だけです。

>>> s = {[1, 2, 3], [4, 5, 6]}
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unhashable type: 'list'

内包表記

 setには内包表記があります。めったに使わないのですが、dictの内包表記と間違えて書かないために覚えておく必要があります。

>>> s = {i**2 for i in range(5)}
>>> print(s)
{0, 1, 4, 9, 16}

frozenset

 これも知っておいた方が良いでしょう。

 集合の中に集合を入れる、あるいはsetを辞書のキーにする、という場面がたまにあります。でも、setはmutableなので、そのままでは使えません。

>>> s = {{1, 2, 3}, {4, 5, 6}}
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unhashable type: 'set'

 この場合、frozensetが使えます。これも組み込み型です。listに対するtupleだと思えば良いでしょう。

>>> s = {frozenset({1, 2, 3}), frozenset({4, 5, 6})}
>>> print(s)
{frozenset({1, 2, 3}), frozenset({4, 5, 6})}

まとめ

 Pythonの基本的なコレクション型4つについて説明してみました。

 この記事の内容程度のことを知っておくと、コーディングがはかどります。

*1:これも説明が難しい概念ですが、わからなければ頑張って調べてください。この記事では触れません。

*2:iterableなら何でもいいのですが、iterableが何であるのかについてもこの記事では触れません。

*3:3.6でCPythonの実装が変わり、3.7から正式な言語仕様になりました

【python】code.pyも作っちゃだめだよという話

概要

 実行時カレントディレクトリにcode.pyを置いておいたら訳のわからない落ち方をした。

現象

 なんかimportしたらまともに動かなかった。調べると、pytest→pdb→codeという流れでimportして、自作のcode.pyはエラーが出るコードだったのでそこで止まってた。

 ん、codeなんかあったっけ? と思って調べたら、

code --- インタプリタ基底クラス — Python 3.7.4 ドキュメント

 あるんかーい! はじめて知ったけど、中身を見るとこんなのユーザは使わないよというものだったので知らなくてもしょうがないと思った。

 要らないファイルだったので、自作のcode.pyを消して解決。

まとめ

 test.pyは有名ですが、code.pyはまったく知名度がないのにまったく同じ現象が起きるので、厄介だと思った。うっかり作る人が普通にいそう。

【python】標準データ型での二次元配列の表現あれこれのアクセス速度

はじめに

 俗に言う「二次元配列」をpythonで表現しようとすると、listのlistで書くというのが一番最初に思いつくやり方だと思います。

 速度のこととか考えるとどうやるのがいいのか? ということは実はあまり知らなかったので、この際いろいろ試してみます。

調査対象

 こんなのを考えます。

list of list(s)

 リストのリスト。王道ですな。

dict of dict(s)

 実はdictでもindexを整数にすれば、インデックスでアクセスするぶんにはlistと同じような使い方ができます。メモリ効率は悪いですが。

flat list

 一つのリストで表現して、一行あたりのデータ数をnとして、i*n+jみたいな方法でアクセスする。

flat dict

 listと同じやり方でもいいし、i, jのようなtupleをキーにするとnumpyっぽく使えます。d[0, 1]とか。

実験

 要素取得の時間だけ見ることにします。

 とりあえず、こうやって5種類のデータを作る。アクセス手段も用意する。

def generate_data(n, m):
    data1 = [[None for _ in range(m)]
             for _ in range(n)]
    data2 = {i:{j:None for j in range(m)}
             for i in range(n)}
    data3 = [None for _ in range(n*m)]
    data4 = {i:None for i in range(n*m)}
    data5 = {(i, j):None for i in range(n) for j in range(m)}
    return data1, data2, data3, data4, data5

def access1(a, i, j, m=None):
    return a[i][j]

def access2(a, i, j, m=None):  
    # 本当にmが要るのはこれだけだが、他も合わせないと面倒なのでキーワード引数に
    return a[i*m + j]

def access3(a, i, j, m=None):
    return a[i, j]

 あとは素直に時間を測る。

import time
from itertools import product

def generate_datas(n, m):
    data1 = [[None for _ in range(m)]
             for _ in range(n)]
    data2 = {i:{j:None for j in range(m)}
             for i in range(n)}
    data3 = [None for _ in range(n*m)]
    data4 = {i:None for i in range(n*m)}
    data5 = {(i, j):None for i in range(n) for j in range(m)}
    return data1, data2, data3, data4, data5

def access1(a, i, j, m=None):
    return a[i][j]

def access2(a, i, j, m=None):
    return a[i*m + j]

def access3(a, i, j, m=None):
    return a[i, j]

def main():
    names = ["list of list", "dict of dict", 
             "flat list", "flat dict1", "flat dict2"]
    accessors = [access1, access1, access2, access2, access3]
    for n in [10, 100, 500, 1000]:
        print(n)
        datas = generate_datas(n, n)
        for name, data, acc in zip(names, datas, accessors):
            # 転置なし
            t1 = time.time()
            for _ in range(10):
                for i, j in product(range(n), range(n)):
                    acc(data, i, j, n) 
            t2 = time.time()
            print(f"{name:12}", "N", f"{t2 - t1:.6f}")

            # 転置してアクセス
            t1 = time.time()
            for _ in range(10):
                for i, j in product(range(n), range(n)):
                    acc(data, j, i, n) 
            t2 = time.time()
            print(f"{name:12}", "T", f"{t2 - t1:.6f}")

if __name__ == "__main__":
    main()

 結果。

10
list of list N 0.000175
list of list T 0.000168
dict of dict N 0.000190
dict of dict T 0.000174
flat list    N 0.000170
flat list    T 0.000171
flat dict1   N 0.000178
flat dict1   T 0.000197
flat dict2   N 0.000208
flat dict2   T 0.000206
100
list of list N 0.015498
list of list T 0.014792
dict of dict N 0.014171
dict of dict T 0.018824
flat list    N 0.015062
flat list    T 0.015838
flat dict1   N 0.018054
flat dict1   T 0.017563
flat dict2   N 0.017370
flat dict2   T 0.018839
500
list of list N 0.347277
list of list T 0.360864
dict of dict N 0.397702
dict of dict T 0.622734
flat list    N 0.395908
flat list    T 0.428421
flat dict1   N 0.463291
flat dict1   T 0.552635
flat dict2   N 0.568813
flat dict2   T 0.812436
1000
list of list N 1.410052
list of list T 1.541770
dict of dict N 1.608992
dict of dict T 2.762586
flat list    N 1.595022
flat list    T 1.725559
flat dict1   N 1.846365
flat dict1   T 2.526936
flat dict2   N 3.238760
flat dict2   T 4.195801

 得られる知見としては、

  • flatな表現は遅い。おそらくintのインデックス計算、またはtupleオブジェクトの生成が遅いから。
  • dict表現版は小さいテーブルならそこそこいけるが、大きくなると辛い。また、転置してアクセスすると劇的に遅い(おそらくキャッシュなどの絡みで)

 といったあたりがあり、いずれにせよほぼ常に最速だったのはlist of list(s)なので、結論としては素直にそれで良いということになりそうです。

 つまらない結論ですね・・・

結論

 list of list(s)が強かったです。

【python】numpyが入ってない標準Pythonでnanをゲットする方法

はじめに

 nanの値を取得したいときは、普通はnumpyを使うと思います。

>>> import numpy as np
>>> np.nan
nan

 まあ、そんなシチュエーションそもそもあまりないという話ですが。じゃあ、numpyがないときは?

 即答できる人はあまりいないと思います。

リテラルでnanと書いても受け付けられない

 できそうですが、できません。

>>> nan
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'nan' is not defined

 変数名とみなされます。しょうがないですね。

nanはfloatである

 nanは浮動小数点数の規格(IEEE 754 - Wikipedia)で定義されています。つまりPythonにおいてはfloatです。

>>> type(np.nan)
<class 'float'>

 標準の型で表現できる訳で、あとはどうやって連れてくるかですね。

mathから呼ぶ

 標準のmathモジュールはありがたいことに、nanを属性として持っています。numpyと同じです。

>>> import math
>>> math.nan
nan

 この方法がスマートでしょうか。でも、そのためだけにimportするのがだるいときもありますよね。

文字列→floatの変換で作る

 たとえば、

>>> int("3")
3

 のように文字列型→整数型の変換ができることは皆さんご存知だと思います。floatでも同じことができて、

>>> float("3.14")
3.14

 という操作が可能です。

 ここで理解した人もいると思いますが、以下のようにするとnanをゲットできます。

>>> float("nan")
nan

 タイプ数が若干多いですが、import不要です。でも、ちょっとトリッキーかも。あと、パフォーマンス上は当然不利なはずです。

便利に使う

 ゲットしたnanを変数に入れておくと、リテラルっぽく使えます。同名変数を定義しないのは当然ですが。

>>> nan = float("nan")
>>> [1.0, nan, 3.0]
[1.0, nan, 3.0]

 でもかえって可読性を落とす(nanの宣言から離れていると一見動かなさそうに見える。あるいは、nanのリテラルがあるという勘違いを生む)気もするので、使わない方が良いかも。

まとめ

 というようにゲットできますので、numpyないんだけどnanを扱わないといけないという悪夢のような目に遭っている人は試してみてください。それか、おとなしくnumpyをインストールしてください。

【python】bool(nan)とかnanをastype(bool)するとTrueになるので気をつけよう

なんのことなのか

 タイトルの通りです。

>>> import numpy as np
>>> bool(np.nan)
True
>>> np.array([np.nan]).astype(bool)
array([ True])

 いやまあ、確かにPythonの言語仕様上そうなんですが、釈然としない気も・・・

なんで困るのか

 0かそれ以外かをそのままboolとして扱いたい時があります。

 返り値がnanになりえる関数を使った後、astypeでboolにすればいいと思っているとハマり得ます。

なんてこった……

 nanとか面倒くさいので、タイムマシンがあったら仕様を考えた人に会いに行って、説得すると思います。

 いや、これに関してはPythonの言語仕様のせいかもしれませんが……

【python】sklearnでQuadraticDiscriminantAnalysis(二次判別分析)を試す

はじめに

 線形判別分析は非線形な分布に対応できないのでだいたいイマイチなパフォーマンスになるのですが、QDA(二次判別分析)だと若干緩和されます。

 二次判別分析はその名の通り分離境界が二次関数になります。ということは、非線形性はありますが、大した非線形ではないので複雑な分布には対応できません。

 まあ、一応やってみます。

実験

 以下のようなコードで、circles, moons, xorを試してみました。例によってこの辺を参考にしています。

Classifier comparison — scikit-learn 0.21.3 documentation

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.colors import ListedColormap
from sklearn.datasets import make_moons, make_circles
from sklearn.discriminant_analysis import \
    LinearDiscriminantAnalysis, QuadraticDiscriminantAnalysis

def make_xor():
    np.random.seed(0)
    x = np.random.uniform(-1, 1, 300)
    y = np.random.uniform(-1, 1, 300)
    target = np.logical_xor(x > 0, y > 0)
    return np.c_[x, y], target

def main():
    lda = LinearDiscriminantAnalysis()
    qda = QuadraticDiscriminantAnalysis()

    datasets = [
        ("circles", make_circles(noise=0.2, factor=0.5, random_state=1)),
        ("moons", make_moons(noise=0.3, random_state=0)),
        ("xor", make_xor())]
    

    fig, axes = plt.subplots(nrows=2, ncols=3, figsize=(16, 9))
    cm_bright = ListedColormap(['#FF0000', '#0000FF'])
    cm = plt.cm.RdBu
    
    for i, (cname, clf) in enumerate([("LDA", lda), ("QDA", qda)]):
        for j, (dname, (X, y)) in enumerate(datasets):
            clf.fit(X, y)
            x_min, x_max = X[:, 0].min() - .5, X[:, 0].max() + .5
            y_min, y_max = X[:, 1].min() - .5, X[:, 1].max() + .5
            h = 0.1
            xx, yy = np.meshgrid(np.arange(x_min, x_max, h),
                                 np.arange(y_min, y_max, h))
            Z = clf.predict_proba(np.c_[xx.ravel(), yy.ravel()])[:,1]
            Z = Z.reshape(xx.shape)

            axes[i, j].scatter(X[:,0], X[:,1], c=y, cmap=cm_bright)
            axes[i, j].contourf(xx, yy, Z, cmap=cm, alpha=.2)
            axes[i, j].set_title(cname + " " + dname)
    plt.savefig("result.png")

if __name__ == "__main__":
    main()

結果

 こうなりました。

result.png
result.png

 円形のデータ、XORは二次多項式で分離境界を表現できます。これはロジスティック回帰でやったときもわかっていたことです。

非線形がなんだ! ロジスティック回帰+多項式でやってやる! - 静かなる名辞

 しょせん2次なので、moonsは厳しいというかこれなら線形でやったほうが良いかもな結果になっています。まあ、仕方ないですね。

 実際問題として、特に高次元になれば複雑な分離境界はそこまで必要ないことが多い気がするので、二次程度で済ませるというのはいい選択です。そういう意味では役に立ちますが、SVMの多項式カーネルなどもあるので素のQDAを使うかは微妙、といったあたりでしょうか。

まとめ

 使いみちが難しそうですが、面白いのでうまく活用してみたい気もします。

TypeError: list indices must be integers or slices, not ***等の原因と対処法

はじめに

 pythonを触り始めたばかりの人は、よくこんなエラーに遭遇すると思います。

  • TypeError: list indices must be integers or slices, not ***

 ***の部分はfloatだったりlistだったりstrだったりといろいろありますが、とにかくこんなエラーです。

 また、次のも見たことがあるかもしれません。

  • TypeError: slice indices must be integers or None or have an __index__ method

 これはスライス(:を使ってリストの特定範囲だけ抜き出すやつ)を使ったときに出てきます。まあ、似たようなものです。

 この記事ではこういうエラーの原因と対処法について説明します。

原因

 さて、簡単にこのエラーを再現してみましょう。

>>> lst = [0, 1, 2, 3]
>>> lst["hoge"]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: list indices must be integers or slices, not str

 出ましたね。

 今回は***の部分はstrになっていますが、これは「indices」([]の中に入るもの)として渡されたものが文字列で、でもlistのindicesは整数かスライスじゃないとだめだよ、というエラーです。

 また、スライスについても同様で、このように再現できます。

>>> lst["fuga":]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: slice indices must be integers or None or have an __index__ method

 もうある程度は気づいたと思いますが、[](添字表記と言ったりします)の中に入れられるものは型が決まっています。そして、それ以外の型のオブジェクトを入れるとエラーになります。

対処法

 上述のような理由で出てくるので、はっきり言って対処法はケースバイケースです。なので、状況に応じてデバッグをする必要があります。

 とりあえず、変数を使っているようなケースでは、printしてみましょう。一番単純なデバッグです。

>>> for i in "hoge":  # エラーになってしまう
...     print(lst[i])
... 
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
TypeError: list indices must be integers or slices, not str
>>> for i in "hoge":
...     print(type(i), i)  # iの型と値をprintするコードを追加
...     print(lst[i])
... 
<class 'str'> h  # str型のhが渡っていることがわかる
Traceback (most recent call last):
  File "<stdin>", line 3, in <module>
TypeError: list indices must be integers or slices, not str

 これを見ればなんとなくわかることもありますが、もう少し想像力が必要なケースも多いでしょう。

 ありがちなのは、インデックスでループさせたつもりだったけど元のリストをループさせていたとかでしょうか。

>>> s = ["hoge", "fuga"]
>>> for i in s:
...     print(s[i])
... 
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
TypeError: list indices must be integers or slices, not str

 pythonのforはループ対象から1つずつ要素を取り出す、という動作をします(他の言語のforeachなどを想像されるとわかりやすいかもしれません)。

 この場合は、以下のように書きます。

>>> s = ["hoge", "fuga"]
>>> for i in range(len(s)):  # rangeとlenを組み合わせてインデックスを作る
...     print(s[i])
... 
hoge
fuga
>>> for x in s:  # 単にこれも可
...     print(x)
... 
hoge
fuga

 rangeは「0から引数の整数-1のイテラブル(ループで要素を取り出して使えるもの)を作るクラス」、lenは「イテラブルの長さを取得する関数」ですね。下の方法の方がスマートですが、インデックスを使った複雑なループでは上の方法にも分があるケースがあります。

 あるいは、「割り算や掛け算の結果をインデックスに使ってしまった」場合も、こういうエラーは起こりえます。特に割り算には注意です。

>>> lst = list(range(10))
>>> lst
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> for i in range(10):
...     print(lst[i/2])
... 
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
TypeError: list indices must be integers or slices, not float

 「整数/整数」は浮動小数点数を返します。整数で結果を得たいときは//を使うか、int, math.floor, math.ceil, roundなどを使って丸めてください。

math --- 数学関数 — Python 3.7.4 ドキュメント
組み込み関数 — Python 3.7.4 ドキュメント

>>> for x in range(10):
...     print(lst[x//2])
... 
0
0
1
1
2
2
3
3
4
4

 また、掛け算はどちらかのオペランドが浮動小数点数なら結果も浮動小数点数になりますので、やはりint型への変換が必要になります。

似たようなもの

 大半のイテラブルは添字表記で整数型しか使えません。

>>> (0,1,2)["hoge"]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: tuple indices must be integers or slices, not str
>>> "hoge"["hoge"]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: string indices must be integers

 対処の方針は同じです。

 辞書のつもりでリストを作成していた、あるいはsetを作成していたというケースもありそうです。そういったケースでは、そもそも添字表記の対象とするオブジェクトの作り方が間違っていないのかというところから見直す必要があります。

まとめ

 わかってしまえばなんてことはないエラーなのですが、ぱっと見なにを言っているのかよくわからないので戸惑う、ということも初心者の方にはありがちだと思います。とにかくlistや大半のイテラブルは添字表記に整数、またはスライス(これも整数しか受け付けません)しか受け付けない、そこに整数以外が入るとエラーになる、ということを理解してコードを書くことが大切だと思います。

sklearnで正則化回帰(Ridge, Lasso, ElasticNet)するときはCV付きのモデルがいいよ

はじめに

 正則化回帰は割と定番のモデルなのですが、sklearnのAPIリファレンスをよく見ると、CVが末尾についたモデルがあることがわかります。

  • Lasso→LassoCV
  • Ridge→RidgeCV
  • ElasticNet→ElasticNetCV

API Reference — scikit-learn 0.21.2 documentation

 なんのこっちゃと思っていたのですが、このCVはCross Validation、要は交差検証です。正則化回帰では正則化パラメータを定める必要があるのですが、一概にどの値が良いとは言えないので、パラメータチューニングを行う必要があります。CV付きのモデルは内部的にチューニングを行ってくれるようです。

 どうせチューニングするので、使えるものは使った方が良さそうにも思います。でも、GridSearchCVという見慣れたモデルもあるので、そちらとどちらが良いのか気になりますね。

 試してみましょう。

実験

 Ridgeで、3次多項式の回帰。係数は適当

import time
import numpy as np
from sklearn.preprocessing import PolynomialFeatures
from sklearn.linear_model import Ridge, RidgeCV
from sklearn.model_selection import KFold, GridSearchCV
from sklearn.pipeline import Pipeline
from sklearn.metrics import r2_score

def main():
    np.random.seed(0)
    x = np.arange(-5, 5, 0.3)
    y = 7 + 5*x + 3*x**2 + 1*x**3 + np.random.normal(scale=3, size=x.shape)
    X = x.reshape(-1, 1)

    x_test = np.arange(-7, 7, 0.1)
    y_test = 7 + 5*x_test + 3*x_test**2 + 1*x_test**3
    X_test = x_test.reshape(-1, 1)
    
    ridge_origin = Pipeline(
        [("pf", PolynomialFeatures(degree=3, 
                                   include_bias=False)),
         ("r", Ridge())])

    ridge_cv = Pipeline(
        [("pf", PolynomialFeatures(degree=3,
                                   include_bias=False)),
         ("r", RidgeCV(cv=KFold(n_splits=6, shuffle=True, random_state=0)))])

    ridge_grid = GridSearchCV(
        Pipeline(
            [("pf", PolynomialFeatures(degree=3,
                                       include_bias=False)),
             ("r", Ridge())]),
        param_grid={"r__alpha":[0.1, 1.0, 10.0]},
        cv=KFold(n_splits=6, shuffle=True, random_state=0))
    
    for name, reg in [("origin", ridge_origin), 
                      ("CV", ridge_cv), 
                      ("GridSearchCV", ridge_grid)]:
        t1 = time.time()
        reg.fit(X, y)
        t2 = time.time()
        fit_time = t2 - t1
        y_pred = reg.predict(X_test)
        score = r2_score(y_test, y_pred)
        print("{0:13} fit_time:{1:.6f} r^2:{2:.6f}".format(
            name, fit_time, score))

    print(ridge_cv.named_steps.r.alpha_)
    print(ridge_grid.best_estimator_.named_steps.r.get_params()["alpha"])

if __name__ == "__main__":
    main()

 結果。

origin        fit_time:0.002750 r^2:0.999419
CV            fit_time:0.031689 r^2:0.999850
GridSearchCV  fit_time:0.053099 r^2:0.999850
10.0
10.0

 RidgeCVでやろうとGridSearchCVでやろうと同じ結果に落ち着いているようですが、RdigeCVの方が微妙に速いです。

 考察すると、やはりGridSearchCVだと汎用的なぶん余計なオーバーヘッドが多く、RdigeCVの方は高速に実行できるようです。あと、n_jobsを指定したら理不尽なほど遅くなったので、そういう方向で高速化も無理だと思います。

まとめ

 こういうものがある、ということを知っておくと、パラメータチューニングが捗るかと思います。あと、LogisticRegressionCVもあったりするので、分類でも使えます。

 なんでもかんでもGridSearchCV、というのではなく、使えるモデルを使い分けるという姿勢が大切だと思いました。

【python】相関係数行列をstatsmodelsを使って描く

はじめに

 相関係数行列を描く方法としては、pandasとseabornを使う方法などが一般的です。しかし、statsmodelsで行う方法も実は存在します。

pandas+seabornでやる場合

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns

np.random.seed(0)
df = pd.DataFrame({"A":np.random.randint(0, 10, size=(10,)),
                   "B":np.random.randint(0, 10, size=(10,)),
                   "C":np.random.randint(0, 10, size=(10,))})

df_corr = df.corr()
sns.heatmap(df_corr, vmax=1, vmin=-1, center=0)
plt.savefig("pandas_cm.png")

pandas_cm.png
pandas_cm.png

 参考:pandas.DataFrameの各列間の相関係数を算出、ヒートマップで可視化 | note.nkmk.me

 というか相関係数の記事を書くときにstatsmodelsのリファレンスを検索していたらこれを見つけたので、試してみようと思った次第です。どれくらい使い物になるのでしょうか。

使い方

 ずばり、これです。

statsmodels.graphics.correlation.plot_corr — statsmodels v0.10.1 documentation

 名前が覚えづらいのが最大の難点で、他は普通に使えます。というか、seaborn.heatmapとそんなに変わりません。

 公式で紹介されている使い方。

>>> import numpy as np
>>> import matplotlib.pyplot as plt
>>> import statsmodels.graphics.api as smg
>>> hie_data = sm.datasets.randhie.load_pandas()
>>> corr_matrix = np.corrcoef(hie_data.data.T)
>>> smg.plot_corr(corr_matrix, xnames=hie_data.names)
>>> plt.show()

 相関行列の計算からやってくれるわけではなく、例ではnumpyで計算しているようです。

 これを踏まえて書いたコード。

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from statsmodels.graphics.correlation import plot_corr

np.random.seed(0)
df = pd.DataFrame({"A":np.random.randint(0, 10, size=(10,)),
                   "B":np.random.randint(0, 10, size=(10,)),
                   "C":np.random.randint(0, 10, size=(10,))})

df_corr = df.corr()
plot_corr(df_corr, xnames="ABC")
plt.savefig("statsmodels_cm.png")

 満足の行く図にするためには、最低限xnamesを指定する必要があります。seaborn.heatmapでいうxticklabels, yticklabelsですね。iterableを渡せば良いようです。

 結果。

statsmodels_cm.png
statsmodels_cm.png

 見た目が少し違いますね。デフォルトのカラーマップはこっちの方が良いかも。

 それだけ? と思ってリファレンスを見直したら、面白そうな引数が2つだけありました。

title
str, optional
The figure title. If None, the default (‘Correlation Matrix’) is used. If title='', then no title is added.

normcolor
bool or tuple of scalars, optional
If False (default), then the color coding range corresponds to the range of dcorr. If True, then the color range is normalized to (-1, 1). If this is a tuple of two numbers, then they define the range for the color bar.

 normcolorはカラーマップの範囲を-1~1に一致させてくれるので、その方が良いと思います。同じことをseabornでできないか調べましたが、ぱっと見なさそうなので、アドバンテージです。

 両方設定してみましょう。
 

plot_corr(df_corr, xnames="ABC", title="random", normcolor=True)

statsmodels_cm.png
statsmodels_cm.png

 それなりに満足感のある結果。

まとめ

 どうということはありませんが、欠点もなさそうだし、使いやすいのでこちらでやってもいいはず。

 難点は、やはり名前が覚えづらいこと(importを書くときに迷う)くらいでしょうか。

ブログで直帰率が高いことは問題ではない。満足して帰っていれば

はじめに

 ブログをある程度真面目に運営している人は、googleアナリティクスなんかを入れて色々な指標を日々確認していると思います。指標が悪いと、なんか問題があるのではないかと思いがちです。

 当ブログは直帰率が高いです。8割以上といったところです。でも、気にしていません。どうして気にしないのか? 気にする意味がないからです。

 いや、真面目な話、気にしていた時期もあったといえばあったんですよ。レイアウトの改善で1%くらい下がったかな? ということをやっていた時期もありましたが、最近はきっぱり諦めました。それに伴って、他の要因*1でレイアウトをいじっているので、直帰率もきもち程度上昇気味です。振れ幅は数%ですけどね。

どうして直帰率が高いんだ!

 A:技術ブログだからさ

 あー、えーっと、この記事は「プログラミングの記事を書くかたわら、気晴らしで書いている運営報告系雑記記事」です。なので、検索から来た人の多くにとって、自分のブログには直接は当てはまらないと思います。ごめん。

 必要だと思うので技術ブログというブログの形態について説明しておくと、プログラミングをやる上でのお悩み解決に役立つような記事を中心に上げるブログです(勝手に定義)。「このコマンドの使い方がわからないなー」とか「書いて動かしたらこんなエラーが出た」とか、そんな動機でググった人がやってくるブログです。

 薄々感づいてきた人もいるかもしれませんが、当ブログにやってくる人のほとんどは別に当ブログを読みたい訳ではなくて、暇つぶしに読むブログを探しているとかでもなくて、単に自分の抱えている問題を解決したいだけです。私のブログが解決策を示せていれば、満足して直帰します。残念ながら解決につながらなかった場合も、直帰して他のサイトを読みに行きます。

 それでも1割くらいの人は直帰しないで回遊してくれていますが、それは3パターンくらいあって、

  1. 記事中に貼った内部リンク(関連する内容の記事へのリンク)を見てくれる
  2. 仕事中とかにあまりやる気がない状態でやってきて、仕事をしたくないので漫然と最新記事とか記事下の関連コンテツを回遊している(失礼)
  3. 「おぉ~このブログいろいろ書いてあるぅ~」と思ってくれてアーカイブやカテゴリページを総なめしている。ログを見ると、そんな感じで回遊している人が毎日1~2人はいそうな気がします。大変ありがたいです

 のいずれかだと思います。まあ、どれにしてもそんなに増えることは期待できない訳です。
(せいぜい記事中の内部リンクを増やすくらい。でも内部リンクを貼る必要のない記事に貼ってもしょうがないのだし)

 少し話が脱線しましたが、「知りたいことについてググる」「問題の解決策を探す」といったような、いわゆる『調べごと系』の検索ニーズに応えるというのは、技術ブログに限らず多くのブロガーの方が実践している方向性だと思います。すると何が起こるか? ユーザは目的を達成してもしなくても帰っていきます。なので、直帰率は自ずと高くなる、ということです。

どのみち直帰する?

 もう少し上の現象について掘り下げて考えてみましょう。同じことを二回書きますが、整理すると以下の図式です。

  • ユーザが満足した(検索の目的を達成した)

 それ以上の用はないので直帰する。

  • ユーザが不満足だった(検索の目的を達成しなかった)

 このブログには見切りをつけて他に行く。

 この状況のユーザに対して、直帰されないためにブログ運営者から打てる手はほとんどありません。いくらコンテンツの質を上げようが、内部リンクを工夫しようが原理的にほぼ無理です。最初から「知りたいことが載っているページを探して、読んだら直帰する」つもりで来ている訳ですから。

 それでもウザいくらいに記事中に内部リンクを貼るとかすれば統計的に何%かのユーザは「釣られて」くれるかもしれませんが、そういうのは回遊の押し売りとでもいうべきもので、ブログの運営の仕方として望ましいものではないと思います。

 たとえばランディングページと大して関係のない記事に回遊させたとして、「なんで俺はこんなの読まされたんだろう」と当然思われるでしょう。そういう事態は避けるべきです。ただ、このあたりの塩梅はケースバイケースで、結果的にユーザに許容されるであろうという確信が持てれば「釣って」もいいと思います。「この記事を読みに来た人なら、絶対にこっちの記事も読んだ方が良い」みたいなのは適切な説明をつけてリンクしておくと良いでしょう。でも、そういう「釣って良い」ケースは限られるので、実際問題として個別の記事でそういう処置がとれるものはあったとしても、全体の直帰率に及ぼす影響はさほど大きくないでしょう。

 結論を言うと、「調べごと」系にフォーカスしたブログでは原理的に直帰率は改善できないはずです。高いなら高いままと考えた方が良いです。

だから、直帰率のこととか気にしなくていいと思うよ

 実際問題として、直帰率が低いとなにか問題があるのでしょうか?

 まあ、直帰率の1%の低下は大雑把にはPVを1%増やすかもしれませんが、雀の涙です。そして直帰率を1%下げるというのはけっこうたいへんです。これでPVを稼ごうと考えるより、先にやるべきことがたくさんあるでしょう*2

 直帰率が高いとgoogleの評価が~という話もちらほら見かけますが、眉唾物だと思っています。少なくとも、公式にそれを肯定する情報はありません。

 参考:
サイト直帰率が高くてもGoogle順位には「影響ない」
ページの滞在時間はSEO順位に影響する?コンテンツの滞在時間と直帰率を改善する方法 | プロモニスタ

 だいいち、検索からサイトに飛んだ人をトラッキングする手段は、普通に考えたらないのではないでしょうか。googleアナリティクス(googleのアクセス解析ツール)を入れているサイトなら結果的には情報がgoogleに送信されますし、もしかしたらchromeになにか仕込まれているという可能性も皆無ではありませんが、そういったものを検索の評価に使うというのは現実的ではないように思います。

 あとは、ユーザ満足度やコンバージョンに結びつける話もありますが、ユーザ満足度に関しては上述の通り「満足しようがしまいが結果的に直帰する」ユーザがほとんどである以上、直帰率では測れません。コンバージョンも普通のブロガーには関係ないですよね。

 なので、直帰率は気にしなくて良いということです。

ユーザ満足度はgoogleアナリティクスでは測れない

 さて、ここまで読んできた人の中には、直帰率はユーザ満足度の指標なので改善するべき、という巷で喧伝されている説明を信じていたのに、という人もいるでしょう。もう直帰率はあてにならないと知ってしまった訳で、困りますよね。

 繰り返しになりますが、直帰率はユーザ満足度の指標ではありません。私の感覚では、満足した結果として直帰する、というユーザはかなりの割合を占めます。なので、他の方法で考えるしかありません。

 真っ先に思い浮かぶのは、平均ページ滞在時間でしょうか。でも、あれは直帰したユーザについては計測できない(というか0になる)という欠点を抱えています。駄目駄目ですね。それに、滞在時間が長ければ満足しているという発想にも無理があるでしょう。読みづらくて時間がかかっているのかもしれないし、別ウィンドウやタブで開いたまま放ったらかして他のページを見ている可能性も十分にあります。

 参考:
まだGAの「滞在時間」を信用してるの? 計算の仕組みとその使い方を理解する[第15回] | 衣袋教授の新・Googleアナリティクス入門講座 | Web担当者Forum

 この章のタイトル「ユーザ満足度はgoogleアナリティクスでは測れない」はちょっと過激にしてありますが、ブログに関して言うと当たらずといえども遠からずというのが私の肌感覚です。特に、ページごとに出てくる数字はPV以外「気休め」くらいに思った方が精神衛生上良いでしょう。

 では何で見るのがいいかというと、Search Consoleはある程度使えます。ページごとにどんなキーワードでユーザが来ているのかがわかるので、キーワード(検索意図)とページの内容がずれていたらたぶん満足度は低いだろうな、ということがわかるからです。これは改善に使えます。

 あとはやっぱり、自分で読んでみて読みやすいか、わかりやすいかといったあたりでしょう。

それでも直帰率を下げたい

 そんなに直帰率を下げることにこだわらなくていいという記事ですが、それでも下げたいのであれば。

  1. サイドバーとかに面白いものを出して(人気記事ランキングとか)、そっちに飛ばす
  2. 無理のない範囲で内部リンクを貼って、関連記事への回遊を促す
  3. 記事を読み終えたらすぐに関連記事リストが出てくるように配置する

 の3点くらいでしょうか。数%~5%くらいはいけると思います。私はもう実践していませんが、こだわる方はどうぞ。

まとめ

 ということで、直帰率は下げなくても可です。直帰率は極論すれば、ブログにとっては「下げれば反比例してPVが増える以外、特に意味のない数字」と言っていいと思います。

 ブロガーの方は、直帰率が高いからユーザの不満度が高いのかなぁ~とか心配する暇があったら、せいぜいサチコ*3を見てキーワード最適化しましょう。

*1:ユーザビリティだったり広告最適化だったり

*2:数千記事、100万PVクラスのブログだと事情が違う可能性はあります。でも、そんな人は読んでないだろうし

*3:Search Console

pythonで相関係数を計算する方法いろいろ3種類

はじめに

 pythonで相関係数を計算する方法はいろいろあります。確認したら、主要ライブラリだけで3つありました。

 いろいろあるということは用途によって使い分けられるということなので、淡々と書いていきます。

 なお、念のために断っておくと、ここで書いている「相関係数」はすべて「ピアソンの積立相関係数」です。順位相関などはまた別に調べてください(ただしpandasを使う方法だと出せます)。

 目次

データの確認

 予め以下のようなデータを定義しておきます。

>>> import numpy as np
>>> np.random.seed(0)
>>> x = np.arange(0, 10, 0.1)
>>> y = x + np.random.normal(size=x.shape)

 散布図にプロットして確認。

>>> import matplotlib.pyplot as plt
>>> plt.scatter(x, y)
<matplotlib.collections.PathCollection object at 0x7f31aa415f28>
>>> plt.savefig("fig.png")

fig.png
fig.png

 もう少しサンプル数が少なくても良かったような気もしますが、せっかく定義したのでこれでやります。

numpyでやる

 numpyの場合はnp.corrcoefで相関係数「行列」を出してくれます。

>>> np.corrcoef(x, y)
array([[1.        , 0.94129622],
       [0.94129622, 1.        ]])

 0.9以上なので強い相関があるみたいです。「行列」が出てくるので、単に相関係数がほしいときは適当に取り出します。

>>> np.corrcoef(x, y)[0, 1]
0.9412962237004372

 あまりスマートではないので、本当に相関係数「行列」がほしいときに使います。

numpy.corrcoef — NumPy v1.17 Manual

pandasでやる

 pandasでもnumpyと同じことができるようです。

>>> import pandas as pd
>>> df = pd.DataFrame({"x":x, "y":y})
>>> df.corr()
          x         y
x  1.000000  0.941296
y  0.941296  1.000000

 行と列に名前がついて使いやすくなったと思います。また、ピアソン以外の相関係数も、kendall, spearmanをmethod引数に渡すことができ、なんならcallableで任意の関数で計算することもできるといった使いやすさがあります。多機能ですね。

pandas.DataFrame.corr — pandas 0.25.1 documentation


 あと、相関係数「行列」がほしいときはpandasを経由した方が便利でしょうか。seabornに投げて可視化するときに、行・列の名前を考慮してくれるので、便利そうです。

pandas.DataFrameの各列間の相関係数を算出、ヒートマップで可視化 | note.nkmk.me

scipyを使う

 漢は黙ってscipy、という価値観が私にはあります。

>>> from scipy import stats
>>> stats.pearsonr(x, y)
(0.941296223700437, 5.153124094421605e-48)

 勝手に両側検定をやってp値を出してくれています(結果のtupleの0から数えて1つめ)。

scipy.stats.pearsonr — SciPy v1.3.0 Reference Guide

 検定やってくれるのはいいですね。普通は別途やる必要があると思います。

あと思ったこととか

  • なんで標準のstatisticsで用意されてないの
  • statsmodelsは高度な機能はいろいろ提供しているくせに、ただの相関係数の出し方がいくらググっても出てこないのはなんで。リファレンスすごく読みづらいし。あるかもしれないけど諦めた

まとめ

 まあ、3つあればいいか……行列がほしいときは楽そうなのはpandas、単に数字がほしければscipyという使い分けになりそうですね。

Pythonプロセスの自分自身のメモリ使用量を調べる

 簡単なテストや処理をしているとき、Pythonプロセス自身のメモリ消費量を計算したくなるときがある。やり方を知らなかったけど、頑張って検索したら出てきたのでメモ。

import os
import psutil

process = psutil.Process(os.getpid())
print(process.memory_info().rss)

 参考にしたもの(というか、そのまま)
Total memory used by Python process? - Stack Overflow

 psutilは標準ライブラリではなく、外部ライブラリ。活発に開発されている。インストールは普通にpipでできる。

psutil · PyPI

 試しに実行してみる。

import os
import psutil

process = psutil.Process(os.getpid())
print(process.memory_info().rss)  # => 9891840

lst = list(range(10**6))
print(process.memory_info().rss)  # => 50974720

 単位はバイトで、memory_info()を呼ぶたびにリアルタイムの情報が得られる。便利に使えそうです。

nltkでテキストを文・センテンス単位で分割する

概要

 自然言語処理やテキストマイニングをしていると文単位で処理・分析したいということはたまにあるので、テキスト(複数文)→センテンス(単一の文)という変換をしたくなることがあります。

 英語の場合は、nltkを使うと簡単です。

nltk.sent_tokenizeで一発

>>> import nltk
>>> s = "It's very easy. You should use 'nltk.sent_tokenize()'."
>>> result = nltk.sent_tokenize(s)
>>> result
["It's very easy.", "You should use 'nltk.sent_tokenize()'."]

 簡単ですね。

他の言語でやりたいんじゃ

 第二引数に言語を指定できます。

nltk.tokenize package — NLTK 3.4.4 documentation


 使える言語は以下のものです(punktというリソースを使う必要があり、使う前にpunktを落とす必要があります。エラーメッセージに手順が出てくるので簡単です。また、以下のリストはpunktに含まれる一覧を拾ったものです)。

czech
danish
dutch
english
estonian
finnish
french
german
greek
italian
norwegian
polish
portuguese
russian
slovene
spanish
swedish
turkish

 日本語はないんじゃい・・・正規表現などで地道に分割しましょう。

関連記事:【python】区切り文字を含めてsplitする - 静かなる名辞

まとめ

 英語であればそれなりに満足の行く結果が得られますので、いいと思いました。

指数関数を二次多項式で近似してみる

はじめに

 指数関数って右半分の形だけなら、二次関数になんとなく似ていますよね。二次多項式を持ってくれば近似的にできそうな気ができるので、やってみましょう。

  y=e^x y=a + bx + cx^2はだいたい同じようなものじゃないの? という話です。

プログラム

 pythonのscipyを使います。コードを載せますが、読み流して頂いても構いません。

import numpy as np
import matplotlib.pyplot as plt
from scipy.optimize import curve_fit

def exp_f(x):
    return np.exp(x)

def pow_f(x, a, b, c):
    return a + b * x + c * x ** 2

def main():
    np.random.seed(0)
    x = np.arange(0, 3, 0.05)
    y = exp_f(x) + np.random.normal(scale=1, size=x.shape)

    params, cov = curve_fit(pow_f, x, y)
    plt.scatter(x, y, c="b", alpha=0.2)

    plot_x = np.arange(0, 4, 0.1)
    plt.plot(plot_x, exp_f(plot_x), color="b", label="y=exp(x)")
    plt.plot(plot_x, pow_f(plot_x, *params), color="r", 
             label="y={0:.2f}+{1:.2f}x+{2:.2f}x^2".format(*params))
    plt.legend()
    plt.savefig("result.png")

if __name__ == "__main__":
    main()

result.png
result.png

 内挿で見るとぱっと見なんとなくフィットしているような気もしますが、外挿は案の定だめです。

 でも、限られた区間であればこれでいいような気も・・・

できる理由

 指数関数をテイラー展開すると、多項式になるからです。更に、低い次数のものほど係数が大きい多項式なので、低次の多項式でけっこううまく近似できます。有名な話ですね。

テイラー展開(exp)

 こちらのページでぽちぽち動かしてみると実感できます。

記事の寿命から考える、1記事で1日に得るべきPVとブログの収益性

はじめに

 当ブログは見ての通りたくさん広告を貼っていますが、こういうことをしていると「どれくらいPVを稼げば、記事を書く労力に対して儲けが割に合うのかなぁ」ということが気になってきます。そこで常日頃から考えていたことを軽く書いておきます。

 テーマは「1記事で1日にどれくらいPVを稼げていれば、割に合う広告収益が得られるのか」です。ぶっちゃけダーティーなカネの話ですが、ふんわりした感じで計算していきますのでご安心ください。

 目次

未来永劫PVを稼げて広告を貼り続けられるなら、いつかはペイする

 たとえば1ヶ月に1円しか稼がない記事があったとして、1年で12円、10年で120円、100年で1200円の収益が入ります。1ヶ月に1円というのはとても低いハードルなので、たぶん(最低限の内容があれば)すごく適当に書いた記事でも達成できます。こういう方向性で努力するというのも一つの手です。100年待てるのなら。

 ふざけんな、そんな非現実的な想定してなんの役に立つんだ、と思われるかもしれませんが、「どれだけの期間で記事作成に支払った労力を回収したいのか」が重要、というのがミソです。

 また、「現実的にどれくらいの期間、収益を上げられるのか」も当然考慮するべきでしょう。100年の間にgoogleやはてながサービスを終了するかもしれないし、そもそも100年後の人類(人工知能の文明になってるかもしれないけど)の興味関心に沿った記事でなければ読まれないのですから。

記事の寿命は3年程度

 日頃から検索エンジンを使っている人であれば、だいたい数年以内に書かれたページが上位に出てくるということは実感していると思います。それ以前に同じテーマで書いた人がいないのかというとそういう訳ではなく、古い記事は検索ランキングを落とされます。何十位もさかのぼれば、古めのページも出てきたりします。

 「どれくらいの期間で投入した労力を回収するのか」をこれから自動的に決めることができます。というか、決めないといけません。

 今回は考察を簡単にするために、3年間は一定のアクセスを集めて、それ以後はアクセスが0になる、というモデルを想定しましょう。この想定はそんなに実情からかけ離れている訳ではなく、どちらかといえばやや保守的な想定です(実際には記事を公開してから数年が過ぎ、検索ランキングが落ちてもある程度のアクセスは入ってきます。でも、0とします)。

PVの単価を考えると1PV 0.15円くらい

 ダーティーな話題ですが、避けては通れない話題です。1PVあたりいくら稼げるのかという問題です。

 たとえばgoogleアドセンスで収益化するのなら、500PVくらいに1回は広告を踏む人がいて、30円とかが懐に入ります。アフィリエイトなら、PVに対する商品の売れる数の比率は数分の1とか未満になるけど、一回で懐に入る金額は安くても30円の10倍とかのはずです。

 なので、平均的に見ると1PVでいくら稼いでいる、という数字を計算できます。

 このブログはアドセンスしか貼っていませんが、実測値をぼんやりと書くと0.1円/PV以上はあるけど0.2PV/円はないかな、というくらいです。今回は0.15円/PVで考えます。これもどちらかといえば保守的な数字ですが、でもそんなに実情から乖離している訳ではありません。
(ただし、この数字はサイト・コンテンツの内容や、広告の貼り方などによってけっこうブレるので、あまり当てになりません。上の数字の半分の人も、倍以上の人もいます。適当に自分が使いたい数字を当てはめてください)

記事に費やした労力をお金に換算すると平均437円くらい

 これもまたダーティーな話題ですが、1記事を作るのに費やした労働コストを金銭換算します。

 計算は簡単な時給換算です。まず最低賃金を見ます。

https://www.mhlw.go.jp/stf/seisakunitsuite/bunya/koyou_roudou/roudoukijun/minimumichiran/

 平成30年度の全国加重平均(ちゃんと書かれていませんが人口で加重平均しているのだと思います)が874円なので、この数字を採用します。最低賃金以下なら割に合わない、という単純な発想です。「俺は最低賃金じゃ働きたくない、せめてン円はほしい」という人は勝手に好きな数字を決めて計算してください。

 時給がわかったので、あとは1記事にどれくらいの時間を費やしたのかを考えます。これも記事の長さによっていくらでも変わってくる数字ですが、勝手に平均30分ということにします。ブログ記事を書くのに費やす時間の実情としては、そんなものでしょう。

 時給と労働時間が決まれば、1記事に投入されたコストがわかります。

 437円ですね。

437円を3年で稼ぐには一日3PV必要

 まず1日あたりに1記事が稼ぐべき金額を決めます。 \frac{437}{365\times 3} \simeq 0.4となります。

 更に0.15円/PVですから、 0.4 / 0.15 \simeq 2.7となり、1日に3PV稼いでればまあまあ割に合うということになります。

 これはそんなに高いハードルではないので、お金目当てでやる人はクリアできるでしょう。ま、当ブログには一日3PVも稼いでいない記事いくらでもありますが。

ただし稼げるとは一言も言っていない

 ブログを書いてそこそこの儲けを出せる人は、たぶん他の方法で稼げば最低賃金よりは多くもらえることの方が多いと思うので、そういう意味では割に合わないでしょう。残業して働いた方が稼ぐ手段としてはマシです。

 また、「ブログ専業でも最低賃金くらいはもらえる」というのも無理があると思います。週40時間労働として、上の想定だと一週間に80記事上げることになります。どう考えても無茶です(そんなにネタがない)。まあ、3PV稼げればと割り切ってゴミ記事を量産してもいいですし、あるいはせめて1記事10PVくらいを目指して1記事に時間をかける方向性もあるかもしれませんが、それでも週30記事くらいなので普通の人には無理です。

 逆に、仕事や学業をしながら一日1記事くらい投稿するというペースだとざっくり言って専業フルタイムでやる1/10くらいの投稿頻度なので、稼ぎも月2万円未満くらいという数字になります。まあ、これもリアルな数字で、大半の「ブロガー」は月数万円くらい稼げていれば十分ペイしているのでそれで満足するべき、という結論になります。

 それだと色々寂しいので、もっと派手に稼ぎたいということで、仮に時給2000円を想定すると、1記事一日6PVくらい。3000円で10PVくらいです。この辺の数字の方が肌感覚に近く、「割に合う」と言っていいのはそれ以上の水準かもしれません。

 また、1PVの収益率はブレのある数字なので、倍かもしれないし半分かもしれない、という問題が現実としてはあります。「1PVで0.15円いけるやろ」と思ってやってみたら半分だった、というのは悲惨なので、気をつけてください。

 あとは、「丸一日かけて書いた渾身の力作がぜんぜんPV稼げない」とか「5分で上げたのが案外伸びた」みたいなのも考慮していません。あくまでも平均的な数字です。なので、記事単位での凹凸はありえます(ポジティブな凹凸は構いませんが、ネガティブな方向に向かうのは当然できるだけ減らすべきです。そう考えると長文力作記事はリスキーですね)。

まとめ

 色々と考えてみましたが、個人的にそこそこの発見だったのは「1記事あたり一日3PVあれば、時給換算で最低賃金超える」というあたりですね。思っていた水準はもっと上だったので、少し驚きました。でも、冷静に考えたらやっぱりもう少し上を狙わないと苦しい、という結論に達しました。

 1記事でどれくらいPVを稼いでいるべきなのか、という数字が出るとサイト運営の上でいい目安になるので、収益化してるブログを持っている人は考慮してもいいんじゃない? と思います。みんな当たり前にやっていて意識すらしないということなのかもしれませんが、そういう観点で語っている人は意外と少ないので書いてみました。

 このブログを読んでいる人で、この情報が役に立つ人はあまりいないと思いますが、もしいたら何かの参考にしてください。役に立たなくても「ふーん、そういう世界なのね」と思って読んで頂けたなら私的には幸いです。