requestsを使って、GETでアクセスすると自動的にリダイレクトされる

日常的に使っていて、このブログでも紹介したことのあるrequestsの話です。
参考:requestsを使って、Webサイトのソースコードを取得する

これまで意識せずに使っていたのですが、requestsでgetすると、リダイレクトがあるページの場合、
自動的にリダイレクトされます。
ドキュメントにもはっきり書いてありますね。
リダイレクトと履歴

例えば、このブログはhttpでアクセスすると、httpsのurlにリダイレクトする設定になっています。
そのため、以下のコードは、”https://analytics-note.xyz/” ではなく、
そこからリダイレクトされて、”https://analytics-note.xyz/”にアクセスします。


import requests
url = "https://analytics-note.xyz/"
response = requests.get(url)
print(response.status_code)
print(response.url)

# 以下出力
200
https://analytics-note.xyz/

status_codeがリダイレクトの302ではなく、200になることや、
urlがhttpsの方に書き換えられていることがわかります。

ちなみにリダイレクトされたページへのアクセス結果は、Responseオブジェクトの、historyというプロパティに、
Responseオブジェクトの配列として格納されます。
今回リダイレクトは1回でしたが、複数回に及ぶ可能性もあるので配列で格納されているようです。


print(response.history)
# 出力
[<Response [302]>]

この自動的にリダイレクトしてくれる仕組みはデータ収集等では非常に便利なのですが、
作業の目的によっては逆に不便です。

リダイレクトして欲しくない時は、allow_redirectsという引数にFalseを渡すことでリダイレクトを禁止できます。


response = requests.get(url, allow_redirects=False)
print(response.status_code)
print(response.url)

# 以下出力
302
https://analytics-note.xyz/

pythonで編集距離(レーベンシュタイン距離)を求める

ごく稀にですが、文字列同士の編集距離を求める必要が発生するのでその時のメモです。

編集距離(レーベンシュタイン距離)とは、二つの文字列がどの程度異なっているかを表す距離の一種です。
Wikipediaにも解説があります。

一方の文字列に対して、1文字の挿入、削除、置換を最低何回施せばもう一方の文字列に等しくなるかで定まります。

pythonでこれを求めるときは、python-Levenshtein というライブラリが使えます。

インストール


pip install python-Levenshtein

使い方


>>> import Levenshtein
>>> text1 = 'Levenshtein'
>>> text2 = 'Lenvinsten'
>>> Levenshtein.distance(text1, text2)
4

pickleを使ってpythonのオブジェクトをファイルに保存する

(注)この記事はscikit-learnのモデルをファイルに保存することを念頭に書いていますが、
pickle自体はscikit-learnのモデル以外のものも直列化してファイルに書き出すことができるモジュールです。

以前の記事で、kerasで作成したmodelを保存したり読み込んだりする方法を書きました。
今回はscikit-learnで作ったモデルを保存してみます。
kerasには専用の関数が用意されていたのですが、scikit-learnにはありません。
そのため、他の方法が必要です。
そこでpython標準ライブラリの pickleが使えます。
ドキュメント

利用方法は、ドキュメントのpickle.dumppickle.loadの説明と、一番下の使用例が参考になります。

clfという変数に、学習済みのモデルが格納されているという想定で、保存と読み込みのコード例を紹介します。
また、保存するファイル名は何でも良いのですが、サンプルコードではclf.pickleとします。

まずは保存。


import pickle
with open("clf.pickle", "wb") as f:
    pickle.dump(clf, f)

次に読み込み。


import pickle
with open("clf.pickle", "rb") as f:
    clf = pickle.load(f)

これで、一度学習したモデルを読み込んで、予測に活用することができます。
scikit-learnで学習したモデルを本番運用するならばほぼ必須の技術です。
(pickle以外の方法を使うという手もありますが、何らかの形での保存と読み込みの手段が必要です)

pandasでgroupbyした時に複数の集計関数を同時に適用する

前の記事の続きです。
pandasでデータフレームをgroupbyした時に使える集計関数
ドキュメントのこの記事で参照した部分のすぐ下に、
Applying multiple functions at once
という段落があります。
実はこれ初めて知りました。
今までグルプごとに個数と、平均と、標準偏差を計算したい、みたいな時は、
groupbyして集計を個別に実施して、その結果をmergeするという非常に面倒なことをずっとやっていました。

それが、aggというのを使うと一発でできるようです。


import pandas as pd
from sklearn.datasets import load_iris

# データフレームの準備
iris = load_iris()
df = pd.DataFrame(iris.data, columns=iris.feature_names)
df["target"] = iris.target
df["target_name"] = df.target.apply(lambda x:iris.target_names[x])
del df["target"]

df.groupby("target_name").agg(["count", "mean", "std"])

出力されるのが次です。(ブログのレイアウトの都合上画像で貼り付けます。)

これは便利です。
また、DataFrameのカラム名が2段になっています。
これをみて、indexだけではなく実はcolumnsでも、MultiIndexが使えることを知りました。

pandas-profilingで探索的データ分析

データ分析をする際に、最初にデータ全体(多すぎる時はサンプルを)を眺めてみるのですが、
その時にpandas-profilingというのを使うと便利なので紹介します。

PyPiのページには見事に何も書かれてません。 
そのため、公式ドキュメントらしいものが欲しい時はリポジトリを見ましょう。

インストール


pip install pandas-profiling

使い方ですが、とりあえず、ボストン住宅価格のデータセットでやってみましょう。


# ライブラリインポート
from sklearn.datasets import load_boston
import pandas as pd
import pandas_profiling as pdp

# データの準備(pandasデータフレームを作る)
boston = load_boston()
df = pd.DataFrame(boston.data, columns=boston.feature_names)

# レポーティング
report = pdp.ProfileReport(df)
report # jupyter notebookuで実行すると、notebook上に表示される。
# ファイル出力
report.to_file("boston.html")

これで、下のhtmlファイルが出力されます。
なお、jupyterで表示した場合も同じ見た目です。
boston

各特徴量のデータ型や分布、欠損値や相関係数などがまとまって出力されて非常に便利です。

ただ、便利すぎて、これだけみて何かすごい分析をやったような気がしてしまうことがあるのでそこだけは注意しています。

20ニュースグループのテキストデータを読み込んでみる

職場にはテキストデータは大量にあるのですが、自宅での学習で自然言語処理にについて学ぼうとすると途端にデータ不足に悩まされます。
そんなわけで青空文庫からデータを持ってくるような記事をこのブログに書いているのですが、
実はscikit-learnの付属のデータセットにもテキストデータはあります。(ただし英語)

Tfidfや、word2vecを動かしてみるには十分なので、それの取得方法を紹介します。
ドキュメントはこちら。
sklearn.datasets.fetch_20newsgroups

インポートして、引数でsubsetを指定することで訓練データとテストデータを入手できます。未指定だと訓練データのみです。両方一度に入手するためにはsubset="all"を指定する必要があります。
僕は始めはそれを知らず引数なしで実行して、訓練データを2つに分けて使ってました。


from sklearn.datasets import fetch_20newsgroups
twenty_train = fetch_20newsgroups(subset="train") # 引数省略可能。
twenty_test = fetch_20newsgroups(subset="test")
# trainとtestを同時に入手したい時は  subset="all" を指定。

# 件数の確認
print(len(twenty_train.data)) # 11314
print(len(twenty_test.data)) # 7532

初回実行時にダウンロードされ、data_home で指定したパスか、デフォルトでは~/scikit_learn_dataにデータが保存されます。
そのため、1回目だけは時間がかかりますが、2回目からは高速です。

twenty_train と、 twenty_test には辞書型で各情報が入ります。
twenty_train.keys()を実行すると、
dict_keys(['data', 'filenames', 'target_names', 'target', 'DESCR'])
と出ます。

DESCR にデータの説明、 target_namesに各ニュースグループの名前が入っているので、それぞれ一度は見ておくことをお勧めします。


# 出力
['alt.atheism',
 'comp.graphics',
 'comp.os.ms-windows.misc',
 'comp.sys.ibm.pc.hardware',
 'comp.sys.mac.hardware',
 'comp.windows.x',
 'misc.forsale',
 'rec.autos',
 'rec.motorcycles',
 'rec.sport.baseball',
 'rec.sport.hockey',
 'sci.crypt',
 'sci.electronics',
 'sci.med',
 'sci.space',
 'soc.religion.christian',
 'talk.politics.guns',
 'talk.politics.mideast',
 'talk.politics.misc',
 'talk.religion.misc']

普段は、 data に入ってるテキストデータの配列と、正解ラベルであるtargetをつかえばOKだと思います。

このほか、BoWに変換済みの、 fetch_20newsgroups_vectorized というのもありますが、
あまり使う機会はないのではないかなぁと思います。
学習目的であれば、前処理なども自分で経験したほうがいいと思うので。

ちなみに、scikit-learnのチュートリアルには、
このデータセットを用いて、tfidfとナイーブベイズでモデルを作るものがあります。
Working With Text Data
(このページは見ていませんでしたが、)僕も自然言語処理を始めたばかりの頃、全く同じような内容で勉強をスタートしたので、非常に懐かしく思いました。
その時の試したファイルはあるので、そのうちこのブログにもまとめ直します。

データから確率分布のパラメーターを推定する

データから、そのデータを生成した背景にある確率分布を推定したいことはよくあります。
正規分布やポアソン分布を仮定するのであれば、簡単ですが、多くの分布では結構面倒です。
そこで、scipyのstatsにある、fitとという便利な関数を使って最尤推定します。

今回はベータ分布を例に取り上げます。
公式ドキュメントはここです。
scipy.stats.rv_continuous.fit
ここ、ベータ関数を使ったサンプルも乗ってるんですよね。
初めて読んだ時はもっと早く読めばよかったと思いました。

それでは、真の分布を設定して、そこからデータを生成し、パラメーターを推定してみます。


# モジュールのインポート
import numpy as np
import matplotlib.pyplot as plt
from scipy.stats import beta

# これから推定したい真の分布
frozen_beta_true = beta.freeze(a=3, b=7, loc=-2, scale=4)
# 真の分布に従うデータを生成
data = frozen_beta_true.rvs(500)

# データから最尤推定 (全パラメーター)
fit_parameter = beta.fit(data)
print(fit_parameter)
# 出力
# (2.548987294857196, 4.380552639785355, -1.946453704152459, 3.1301112690818194)

bの値と、scaleがちょっと乖離が大きいかなと感じられるのですが、
結構妥当な値が推定できました。

経験上、ベータ分布を使いたい時は、取りうる値の範囲が決まっていることが多いです。
そのため、locやscaleは固定して推定を行いたいのですが、
その時は、パラメーターにfをつけて、fitに渡すと、
それらのパラメーターは固定した上で残りを推定してくれます。


# データから最尤推定 (loc と scaleは指定する)
fit_parameter = beta.fit(data, floc=-2, fscale=4)
print(fit_parameter)
# 出力
# (3.1998198349509672, 7.4425425953673505, -2, 4)

かなり真の値に近い結果が出ました。
最後に推定した確率分布の確率密度関数を可視化してみましょう。


# 推定したパラメーターで確率分布を生成
frozen_beta = beta.freeze(*fit_parameter)

# 可視化
plt.rcParams["font.size"] = 14
x = np.linspace(-2, 2, 51)
fig = plt.figure(figsize=(8, 5))
ax = fig.add_subplot(1, 1, 1, xlim=(-2, 2), title="scipyによる最尤推定")
ax.plot(x, frozen_beta_true.pdf(x), label="真の分布")
ax.plot(x, frozen_beta.pdf(x), label="推定した分布")
ax.hist(data, bins=30, alpha=0.7, density=True, label="サンプルデータの分布")
ax.legend()
plt.show()

出力されたのがこちらの図です。
うまく推定されているように見えますね。

pythonを触り始めたばかりの頃は、scipyをうまく使えず、
確率分布はnumpyでスクラッチで書いて、この種の推定もゴリゴリ自分で実装していました。
(かなり効率の悪いアルゴリズムで)
fitを知ってからも、しばらくは4つの戻り値のどれがaでどれがlocなのかよくわからなかったり、
locやscaleを固定する方法を知らず長いこと敬遠していたのですが、
ちゃんとドキュメントを読めば全部書いてあるものです。

BeautifulSoupを使って不要なタグとルビを取り除く

以前の記事で、青空文庫から取得したテキストの文字化けを治しました。
次は、不要なタグを除去します。

正規表現でやってしまえば早いのですが、せっかくなので、BeautifulSoupの使い方の確認も兼ねてこちらを使ってみました。

前提として、
htmlという変数に、銀河鉄道の夜のページのソースが入っているものとします。


# ライブラリのインポートと、soupオブジェクトへの変換
from bs4 import BeautifulSoup
soup = BeautifulSoup(html)

soup.find([タグ名]) や、 soup.find(class_=[class名])で、中のタグを指定することができます。
さらに、get_text()関数を使うと、タグを取り除いた文字列が表示されます。
これで div や h1,h2,…や、a,brタグなど不要タグはほぼほぼ除去できます。
ついでに、不要な前後の空白をstrip()で取り除いて、
300文字を表示してみましょう。


print(soup.find(class_="main_text").get_text().strip()[:300])

# 結果
一、午后(ごご)の授業

「ではみなさんは、そういうふうに川だと云(い)われたり、乳の流れたあとだと云われたりしていたこのぼんやりと白いものがほんとうは何かご承知ですか。」先生は、黒板に吊(つる)した大きな黒い星座の図の、上から下へ白くけぶった銀河帯のようなところを指(さ)しながら、みんなに問(とい)をかけました。
 カムパネルラが手をあげました。それから四五人手をあげました。ジョバンニも手をあげようとして、急いでそのままやめました。たしかにあれがみんな星だと、いつか雑誌で読んだのでしたが、このごろはジョバンニはまるで毎日教室でもねむく、本を読むひまも読む本もないので、なんだかどんなことも

さて、残りは 午后(ごご) などのルビです。

これも不要なので取り除きます。
該当部分のソースコードを見ると、下記のように、ruby, rb, rt, rpの4つのタグがあります。
このうち、 rubyとrbは、タグの中身は残したいので、get_text()で取り除けば十分ですが、rbとrtはタグとその中身を消す必要がります。


一、<ruby><rb>午后</rb><rp>(</rp><rt>ごご</rt><rp>)</rp></ruby>の授業

それには、decompose関数を使用します。


for tag in soup.findAll(["rt", "rp"]):
    # タグとその内容の削除
    tag.decompose()

参考ですが、タグだけを消して、中身を残す時はunwarpを使います。
(昔はreplaceWithChildrenという名前だったメソッドです。pep8対応のためにリネームされたとか。)
hxタグとかbrタグとか、これを使って消してたこともあるのですが、get_text()を使うようになっていらなくなりました。

これで取り除けたはずなので、もう一度本文を表示します。


print(soup.find(class_="main_text").get_text().strip()[:300])

# 結果

一、午后の授業

「ではみなさんは、そういうふうに川だと云われたり、乳の流れたあとだと云われたりしていたこのぼんやりと白いものがほんとうは何かご承知ですか。」先生は、黒板に吊した大きな黒い星座の図の、上から下へ白くけぶった銀河帯のようなところを指しながら、みんなに問をかけました。
 カムパネルラが手をあげました。それから四五人手をあげました。ジョバンニも手をあげようとして、急いでそのままやめました。たしかにあれがみんな星だと、いつか雑誌で読んだのでしたが、このごろはジョバンニはまるで毎日教室でもねむく、本を読むひまも読む本もないので、なんだかどんなこともよくわからないという気持ちがするので

綺麗にルビが消えました。

t-SNEでDigitsを次元圧縮して可視化してみた

特に意図はないのですが、これまで高次元のデータを次元削減して可視化する時はPCAをよく使っていました。
基本的には線形変換なので、非線形な構造を持ってるデータはうまく特徴を捉えられません。
(それはそれで確認する意味があると思いますが。)

最近は、t-SNEという手法を使っている人が多いようなので、やってみたメモです。
irisだとPCAで十分うまく次元削減できてしまうので、今回はdigitsを使います(8*8の手書き数字画像データ)

t-SNEの論文

t-SNE自体の実装は、scikit-leearnを使います。
ドキュメントはここ


# ライブラリインポート
from sklearn.datasets import load_digits
from sklearn.manifold import TSNE
import matplotlib.pyplot as plt

# データ準備
digits = load_digits()
X = digits.data
y = digits.target

# t-SNEの実行
tsne = TSNE(n_components=2)
X_tsne = tsne.fit_transform(X)

# 可視化
x_max, x_min = X_tsne[:, 0].max() * 1.05, X_tsne[:, 0].min() * 1.05
y_max, y_min = X_tsne[:, 1].max() * 1.05, X_tsne[:, 1].min() * 1.05
fig = plt.figure(figsize=(10, 10))
ax = fig.add_subplot(1, 1, 1, xlim=(x_min, x_max), ylim=(y_min, y_max))
ax.set_title("t-SNE")
for i, target in enumerate(y):
    ax.text(X_tsne[i, 0], X_tsne[i, 1], target)
plt.show()

これを実行して表示される画像がこちらです。

一部、変なところに分類されている数字があったり、1が複数のグループに分かれていたりするところはありますが、
非常に見事に分類できています。
これを好んで使う人がいるのも納得です。
高次元のデータの可視化のツールとして提唱されているだけはあります。

ちなみに、PCAで2次元に圧縮したのがこれ。

t-SNEと全く違う結果になっていますね。
(だからといって、PCAという手法自体が劣るというわけではないので注意です。)

pipでライブラリをアップデートする

pipの使い方メモです。

まず、インストール済みのパッケージについての情報は pip list で確認できます。
更新版があるパッケージのみ出力するオプションは -o または --outdatedです。


$ pip list --outdate
Package Version Latest Type
------------------ --------- ---------- -----
alabaster 0.7.11 0.7.12 wheel
astroid 2.0.4 2.1.0 wheel
astropy 3.0.4 3.1.1 wheel
beautifulsoup4 4.6.3 4.7.1 wheel
bleach 2.1.4 3.1.0 wheel
bokeh 0.13.0 1.0.4 sdist
certifi 2018.8.24 2018.11.29 wheel
click 6.7 7.0 wheel
~~~ 以下略 ~~~

アップデートしたいパッケージを決めたら、
pip install に、
 -U か --upgrade のどちらかのオプションをつけてパッケージを指定し実行するとアップデートできます。

例:


$ pip install --upgrade scikit-learn