PythonでURL文字列を構築する

前回の記事がURL文字列の各要素への分解だったので、今回は逆に構築です。

まずはクエリパラメーター(クエリストリング)部分についてです。
今更説明するまではないのですが、クエリパラメーターはキーと値を=(イコール)で結び、さらにそれらを&で繋いでいきます。
自分で実装すると地味に手間なのですが、これもurllibに実装があります。

それが urllib.parse.urlencode です。

試しにやってみます。


from urllib import parse

qs = parse.urlencode(
    {
        "key1": "キー1",
        "value1": "値1",
        "id": "123"
    }
)
print(qs)
# key1=%E3%82%AD%E3%83%BC1&value1=%E5%80%A41&id=123

この例で気づかれたと思いますが、このメソッド、とても気の利いてることにパーセントエンコーディングもやってくれます。
辞書の他に、(キー, 値)のタプルの配列も引数として受け取ってくれます。


qs = parse.urlencode(
    [
        ("key1", "キー1"),
        ("value1", "値1"),
        ("id", "123")
    ]
)
print(qs)
# key1=%E3%82%AD%E3%83%BC1&value1=%E5%80%A41&id=123

クエリパラメーターの文字列を構築するのに、非常に便利ですね。

もう一つ、プロトコルやホスト名、パスなどからURL全体を構築するメソッドもあります。
それが、 urllib.parse.urlunparse です。

scheme, netloc, path, parameters, query, fragment の 6要素のタプルを受け取り、
scheme://netloc/path;parameters?query#fragment の形に結合してくれます。
タプルないの要素は省略は不可で、それを含まないようなURLを作るときは空白文字列を入れないといけません。

正直、そこまで便利には感じませんでした。

一応やってみた例です。


print(
    parse.urlunparse(
        (
            "https",
            "analytics-note.xyz",
            "",
            "",
            "s=統計学",
            "",
        )
    )
)
# https://analytics-note.xyz?s=統計学

上の例で分かる通り、クエリストリングはパーセントエンコーディングやってくれません。
そのため、クエリパラメーターに日本語を含む場合は本当は事前に何らかの手段でパーセントエンコーディングしておく必要があります。

urlunparse はそこまで使うメリットがなさそうなので、単純に文字列の結合を実装しても良さそうです。

PythonでURL文字列を要素に分解する

前回に引き続きurllibの話です。
url文字列ははプロトコル、ホスト名、パス、パラメーターなど、いくつかの要素で成り立っています。
urllibを使うとURLの文字列から簡単に各要素を取り出せます。
使うのはurllib.parse.urlparseです。
ドキュメント: urllib.parse.urlparse

使うのは簡単で、URLの文字列を渡すだけです。結果のオブジェクトのプロパティとして各要素を取り出せます。
次の形で分解してくれます。
URL: scheme://netloc/path;parameters?query#fragment

ドキュメントのページのURLを例にしてやってみましょう。


from urllib import parse

url = "https://docs.python.org/3/library/urllib.parse.html#urllib.parse.urlparse"
result = parse.urlparse(url)

# 結果はParseResultオブジェクト
print(result)
# ParseResult(scheme='https', netloc='docs.python.org', path='/3/library/urllib.parse.html', params='', query='', fragment='urllib.parse.urlparse')

# ホスト名
print(result.netloc)
# docs.python.org

# パス
print(result.path)
# /3/library/urllib.parse.html

上の例では存在しませんが、 query でパラメーターが渡されていた場合、その値をパースしたいこともよくあります。
それには、parse_qs を使います。
ドキュメント: urllib.parse.parse_qs
もしくはparse_qslでも構いません。違いは出力が辞書かリストかです。
ドキュメント: urllib.parse.parse_qsl

パラメーターの多いyahoo ファイナンスさんのURLを拝借してやってみましょう。


url = "https://info.finance.yahoo.co.jp/history/?code=998407.O&sy=2021&sm=1&sd=12&ey=2021&em=1&ed=31&tm=d"
result = parse.urlparse(url)
# queryが取得できた
print(result.query)
# code=998407.O&sy=2021&sm=1&sd=12&ey=2021&em=1&ed=31&tm=d

# parse_qs でパースした結果
print(parse.parse_qs(result.query))
# {'code': ['998407.O'], 'sy': ['2021'], 'sm': ['1'], 'sd': ['12'], 'ey': ['2021'], 'em': ['1'], 'ed': ['31'], 'tm': ['d']}

# parse_qslでパースした結果
print(parse.parse_qsl(result.query))
# [('code', '998407.O'), ('sy', '2021'), ('sm', '1'), ('sd', '12'), ('ey', '2021'), ('em', '1'), ('ed', '31'), ('tm', 'd')]

Pythonでパーセントエンコード(URLエンコード)

パーセントエンコードの詳しい説明はWikipedia参照。
参考: パーセントエンコーディング – Wikipedia

要するに、日本語などURLの中で使えない文字列を表示する方法です。
上記のWikipediaのリンクも、「パーセントエンコーディング」に相当する部分が
%E3%83%91%E3%83%BC%E3%82%BB%E3%83%B3%E3%83%88%E3%82%A8%E3%83%B3%E3%82%B3%E3%83%BC%E3%83%87%E3%82%A3%E3%83%B3%E3%82%B0
になっています。

このようなエンコードと、デコードをPythonで行う方法を調べました。
結論として、url関係のライブラリである、urllibで実装できるようです。

まず、エンコード。
これは、urllib.parse.quote でできます。


from urllib.parse import quote
print(quote("パーセントエンコーディング"))
"""
%E3%83%91%E3%83%BC%E3%82%BB%E3%83%B3%E3%83%88%E3%82%A8%E3%83%B3%E3%82%B3%E3%83%BC%E3%83%87%E3%82%A3%E3%83%B3%E3%82%B0
"""

次に、逆変換(デコード)です。
これは、urllib.parse.unquote でできます。
パーセントエンコーディングだと長いので、サンプルを少し短くしました。


from urllib.parse import unquote
print(unquote("%E3%83%91%E3%83%BC%E3%82%BB%E3%83%B3%E3%83%88"))
"""
パーセント
"""

matplotlibのメモリラベルテキストの回転だけをオブジェクト指向インターフェースで行いたい

主にX軸のメモリが日付などの場合に発生するのですが、matplotlibのメモリテキストが重なって読めなくなることがあります。
それをテキストを回転させて読めるようにする方法のまとめです。

とりあえず、プロットするデータを生成しておきます。


import matplotlib.pyplot as plt
from datetime import datetime, timedelta
import numpy as np
import pandas as pd

# 日付のリスト生成
dates = [datetime(2021, 1, 1) + timedelta(days=i) for i in range(10)]
date_str_list = [d.strftime("%Y-%m-%d") for d in dates]
# ランダムウォークデータ作成
data = np.cumsum(np.random.randn(10))

他サイトなどを見ると、よく用いられているのは、
plt.xticks(rotation=角度)
です。
結果だけ見れば正直これで十分なのですが、
これでも確かに回転できますが、これは、オブジェクト指向インターフェースではなく、ここだけpyplotインターフェースになります。
個人的に、これらを混在させるのは好きではないのであまり好きではありません。


fig = plt.figure(facecolor="w")
ax = fig.add_subplot(1, 1, 1)
ax.plot(date_str_list, data)
plt.xticks(rotation=90)
plt.show()

そしてオブジェクト指向インターフェースで回転させる方法として、よく見かけるのは、
ax.set_xticklabels(xlabels, rotation=角度)
とするものです。
これでもラベルを回転できるのですが、第一引数のxlabelsが省略できず、改めて指定する必要があります。
回転だけやりたいんだというときにこの仕様は面倒です。

前置き長くなりましたが、以上の2個の方法の良いとこどりで、オブジェクト指向インターフェースで回転だけを実行する方法を調べました。
結果として以下の方法があることがわかりました。

ax.tick_params(axis="x", labelrotation=角度)
ax.xaxis.set_tick_params(rotation=角度)
引数はどちらもlabelrotation=でもrotation=でも両方対応しているようです。

それぞれコード例は以下のようになります。


fig = plt.figure(facecolor="w")
ax = fig.add_subplot(1, 1, 1)
ax.plot(date_str_list, data)
ax.tick_params(axis="x", labelrotation=90)
plt.show()

fig = plt.figure(facecolor="w")
ax = fig.add_subplot(1, 1, 1)
ax.plot(date_str_list, data)
ax.xaxis.set_tick_params(rotation=90)
plt.show()

matplotlibのdpiとfigsizeの正確な意味を調べてみた

以前、「matplotlibはdpiの数値を大きくすると画像の解像度が上がる」、みたいな非常に雑な理解で記事を書いたのですが、改めて仕様を確認したのでそのメモです。
参考: matplotlibのグラフを高解像度で保存する
前提として、「dpi」は「dots per inch」の略で1インチあたりのドット数を意味します。
正直、画面に表示される画像のサイズというのは、PCのディスプレイサイズに大きく依存するので、1インチを定義することなんてできないだろうと思って流していたのですが、
実は、 figsizeで指定する数値の単位がインチだったようです。

参考: matplotlib.figure.Figure

figsize(float, float), default: rcParams[“figure.figsize”] (default: [6.4, 4.8])
Width, height in inches.

したがって、 figsize で指定した(横幅, 縦幅) と dpi を掛け合わせると、
生成される図のピクセル数が (横幅*dpi) × (縦幅*dpi) と求まるということのようです。

デフォルトの設定(rcParamsでfigsizeとdpiは確認できる)で、figureを生成して確認してみましょう。


import matplotlib.pyplot as plt

print(plt.rcParams["figure.figsize"])
# [6.0, 4.0]
print(plt.rcParams["figure.dpi"])
# 72.0
fig = plt.figure()
# <Figure size 432x288 with 0 Axes>

ドキュメントによると figsizeのデフォルトは、[6.4, 4.8]、dpiのデフォルトは100のはずなのですが、なぜか僕の環境はそれぞれ、
[6.0, 4.0]と 72になってます。これの原因は不明ですが話の本題とズレるので置いておきます。

生成された図のサイズは、横幅が 6*72=432ピクセル、縦幅が4*72=288ピクセルと、理論通りになりました。

例えば、(滅多にそんな要件はないでしょうが)フルHDサイズのグラフ(1920×1080)が作りたかったら、
figsizeを(16, 9)、dpiを120で作成すれば良いことになります。


fig = plt.figure(figsize=(16, 9), dpi=120)
# <Figure size 1920x1080 with 0 Axes>

文字列のバイト数を取得する

Amazon Comprehend も Amazon Translate も、1度に処理できるテキストサイズの上限が文字数ではなく、バイト数で決まっています。(どちらも5000byte)
とりあえずAPIに投げてエラー処理で対応してもいいのですが、あまり無駄にAPIを叩くようなことをはしない方がいいので、
できるだけ事前にAPIに渡すテキストのサイズが上限を超えていないことを確認することが望ましいです。

そこで、Pythonの文字列の長さをバイト数で取得する方法を調べました。
結論から言うと、バイト列オブジェクトにエンコードして、その後に長さを調べると良いようです。
エンコードには、 str.encodeを使います。

以下のようにして、”走れメロス”が15バイトであることがわかりました。


title="走れメロス"
print(len(title.encode()))
# 15

matplotlibでグラフ枠から見た指定の位置にテキストを挿入する

matplotlibでは、 matplotlib.axes.Axes.text メソッドを指定して、グラフ中にテキストを挿入できます。
このとき、通常は、 ax.text({x座標}, {y座標}, {挿入したいテキスト}) という構文でテキストが入れられます。
ここで言う、 x座標/ y座標は それぞれx軸、y軸に対応した座標になります。当然ですね。
散布図の点や、グラフの頂点に文章を補足するときはとてもありがたい仕様ですが、
その反面、グラフの左上の方、とか中央といった、グラフの中の特定の場所にテキストを入れたい場合はちょと不便です。
それは、グラフの枠の左上や中央の座標が何になるか非自明だからです。

このニーズに対応して、x軸、y軸の座標ではなく、グラフ内部の位置でテキストの位置を指定できることがわかったので紹介します。

ドキュメントの Examples のところに 例があるのですが、transform=ax.transAxesを指定すると、ax.textのx座標、y座標はx軸y軸とは関係なくなり、
グラフの枠の左下を(0, 0), 右上を(1, 1)とする相対座標に変わります。

これを使って、グラフのすみの方や中央に簡単にテキストを置けます。

ちなみに、デフォルトでは、指定した座標の位置に、テキストの左下が当たるように配置されます。
これは、縦位置は verticalalignment{‘center’, ‘top’, ‘bottom’, ‘baseline'(デフォルト), ‘center_baseline’}、
横位置はhorizontalalignment{‘center’, ‘right’, ‘left'(デフォルト)} という引数で変更することができます。
引数名が長いので、それぞれ、va, ha というエイリアスを用意してくれています。
これらの引数は、 transform=ax.transAxes を指定しない時も同じように使えるので覚えておきましょう。

あまり面白い例でなくて恐縮ですが、色々パターンを試したコードとその出力を置いておきます。


import matplotlib.pyplot as plt
import numpy as np

x = np.linspace(-np.pi, np.pi, 101)
y = np.sin(x)


fig = plt.figure(facecolor="w")
ax = fig.add_subplot(1, 1, 1)
ax.plot(x, y)

# 通常は、指定した座標の位置にテキストが挿入される
ax.text(np.pi/2, 1, "$(\pi/2, 1)$")
ax.text(-np.pi/2, -1, "$(-\pi/2, 1)$")

# transform=ax.transAxes を指定すると、図形の枠を基準にした位置にテキストが挿入される
ax.text(0.01, 0.01, "左下", transform=ax.transAxes)
ax.text(0.01, 0.99, "左上", verticalalignment='top', transform=ax.transAxes)
ax.text(0.99, 0.01, "右下", horizontalalignment='right', transform=ax.transAxes)
ax.text(0.99, 0.99, "右上", va='top', ha='right', transform=ax.transAxes)
ax.text(0.5, 0.5, "中央", va='center', ha='center', transform=ax.transAxes)
# 0 ~ 1 の範囲を超えるとグラフの外にテキストを配置できる
ax.text(0.5, 1.05, "上部枠外", ha='center', transform=ax.transAxes)
ax.text(1.02, 0.5, "右側枠外", va='center', transform=ax.transAxes, rotation=270)

plt.show()

出力はこちらです。

transform 引数に他にどんな値を設定しうるのか探したところ、以下のページが見つかりました。
参考: Transformations Tutorial
ax.transData がデフォルトのデータにしがたった座標軸みたいですね。
ax.transAxes の他にも、fig.transFigureやfig.dpi_scale_trans を使って、図全体の中での相対位置で
テキストを配置することもできるようです。
fig.transFigure のほうが、左下が(0, 0)、右上が(1, 1) となる座標で、
fig.dpi_scale_trans はピクセル数を使った具体的な座標指定です。

matplotlibのグラフのx軸とy軸を反転する方法まとめ

先日、matplotlibで作った少し手の込んだグラフを、x軸とy軸を反転したくなることがありました。
少々手こずったのでその時調べた内容をまとめておきます。

matplotlibでは、グラフの種類によってx軸y軸の反転方法が異なります。
大きく分けて次の3パターンがあるようです。

1. x座標とy座標の引数の順番を入れ替えるだけ。
2. x軸とy軸を反転させたバージョンの別のメソッドが用意されている。
3. グラフを書くときに引数で向き(orientation)を指定する。

また、物によっては反転させることができないものもあるようです。

具体的に一つやってみます。
まずデータの準備です。


# 必要ライブラリのインポートとデータ生成
import matplotlib.pyplot as plt
from scipy.stats import beta
import numpy as np

beta_fz = beta(a=3, b=2)
data = beta_fz.rvs(100)
# 真の期待値
x_mean  = beta_fz.stats("m")

そして、ここで生成したデータを元に、以下のグラフと、そのxy軸反転版を書いてみました。
– データのヒストグラム
– 元の分布の確率密度関数
– 元の分布の期待値の位置を表す線
– 元の分布のデータの70%のデータが含まれる区間の塗り潰し


x = np.linspace(0, 1, 101)
x_clip = np.linspace(beta_fz.ppf(0.15), beta_fz.ppf(0.85), 101)

fig = plt.figure(figsize=(12, 6), facecolor="w")

ax = fig.add_subplot(1, 2, 1, title="元のグラフ")
ax.hist(data, density=True, alpha=0.3)
ax.plot(x, beta_fz.pdf(x))
ax.fill_between(x_clip, beta_fz.pdf(x_clip), alpha=0.3, color="orange")
ax.vlines(x_mean, ymin=0, ymax=beta_fz.pdf(x_mean))

ax = fig.add_subplot(1, 2, 2, title="x軸y軸反転したグラフ")
# ヒストグラムは orientation="horizontal" を指定する
ax.hist(data, density=True, orientation="horizontal",  alpha=0.3)
# plot は渡す引数の順番を入れ替えるだけ
ax.plot(beta_fz.pdf(x), x)
# fill_between に対しては、 fill_betweenx メソッドが用意されている
ax.fill_betweenx(x_clip, beta_fz.pdf(x_clip), alpha=0.3, color="orange")
# vlines に対しては、 hlines メソッドが用意されている
ax.hlines(x_mean, xmin=0, xmax=beta_fz.pdf(x_mean))

plt.show()

出力されたグラフが次です。

x軸とy軸が入れ替わりました。 コードをみていただくとそれぞれ反転方法が異なっていることがわかると思います。

x座標とy座標の引数の順番を入れ替えるだけのパターンには、
ax.plot(折れ線グラフ)やax.scatter(散布図)があります。

x軸とy軸を反転させたバージョンの別のメソッドが用意されているパターンには、
棒グラフであれば、ax.bar と ax.barh、
エリアの塗りつぶしであれば、ax.fill_between と ax.fill_betweenx、
縦線であれば、 ax.vlines と ax.hlines
などの組み合わせがあります。
ドキュメントのこちらのページで似てる名前のメソッドを探すと良いでしょう。

グラフを書くときに引数で向き(orientation)を指定するパターンは、
だいたい、ヒストグラム ax.hist だけと考えても良さそうです。
これはそれぞれのメソッドのドキュメントを読めば判断することができます。
(もしかしたら他にもあるかもしれませんが。)

一部のグラフには反転方法がわからないものもありましたが、実用上困ることはなさそうです。

pytablewriterで、pandasのDataFrameからMarkdownの表を生成する

前回の記事に続いて、pytablewriterの機能の紹介です。
参考: pytablewriterでMarkdownの表を生成する

今回はpandasのDataFrameからMarkdownを生成します。
個人的には使う頻度が高いのはこちらです。

まずサンプルとなるデータを作っておきましょう。


import pandas as pd

df = pd.DataFrame(
    data=[[1, 2, 3], [4, 5, 6], [7, 8, 9], [10, 11, 12]],
    columns=['col1', 'col2', 'col3'],
    index=["data1", "data2", "data3", "data4"]
)
print(df)
"""
       col1  col2  col3
data1     1     2     3
data2     4     5     6
data3     7     8     9
data4    10    11    12
"""

早速、このDataFrameをMarkdownに変更します。

利用するのは、 from_dataframe と言うメソッドです。
ドキュメント: 4.4.3. Using pandas DataFrame as tabular data source

次のコードの例のように、 from_dataframe() にDataFrameを渡し、
write_table()メソッドを呼び出すと標準出力にマークダウンが出力されます。


from pytablewriter import MarkdownTableWriter


writer = MarkdownTableWriter()
writer.from_dataframe(df)
writer.write_table()
"""
|col1|col2|col3|
|---:|---:|---:|
|   1|   2|   3|
|   4|   5|   6|
|   7|   8|   9|
|  10|  11|  12|
"""

配列から生成するときはメソッドではなく属性にデータを代入していたのでちょっと使い方が違いますね。

さて、結果をご覧の通り、デフォルトではindexは出力されません。
もしindexが必要な場合は、 from_dataframe()を呼び出す時に、 add_index_column=True を指定する必要があります。


writer = MarkdownTableWriter()
writer.from_dataframe(df, add_index_column=True)
writer.write_table()
"""
|     |col1|col2|col3|
|-----|---:|---:|---:|
|data1|   1|   2|   3|
|data2|   4|   5|   6|
|data3|   7|   8|   9|
|data4|  10|  11|  12|
"""

これでindexも出力されました。

pytablewriterでMarkdownの表を生成する

集計結果を社内に共有する時など、Markdownでテーブルを書く機会はそこそこ頻繁にあります。
(実はBacklogを使っているので、正式なMarkdownとは違うのですが似たようなものです。)

毎回、Vimで置換等を使って書いて、それを貼り付けたりとかしているのですが、
Pythonに pytablewriter という便利なライブラリがあるのを見つけたのでそれを紹介します。
自分の場合は pandas の DataFrameを変換することが多く、それ専用の関数もあるのですが、
まずはこの記事ではドキュメントの Basic usage に沿った基本的な使い方を紹介します。

ドキュメント: 4.1. Basic usage — pytablewriter

ドキュメントの例を参考に、少し書き換えたコードでやってみます。


from pytablewriter import MarkdownTableWriter

writer = MarkdownTableWriter()
writer.table_name = "zone"
writer.headers = ["zone_id", "country_code", "zone_name"]
writer.value_matrix = [
    ["1", "AD", "Europe/Andorra"],
    ["2", "AE", "Asia/Dubai"],
    ["3", "AF", "Asia/Kabul"],
    ["4", "AG", "America/Antigua"],
    ["5", "AI", "America/Anguilla"],
]

writer.write_table()
"""
# zone
|zone_id|country_code|   zone_name    |
|------:|------------|----------------|
|      1|AD          |Europe/Andorra  |
|      2|AE          |Asia/Dubai      |
|      3|AF          |Asia/Kabul      |
|      4|AG          |America/Antigua |
|      5|AI          |America/Anguilla|
"""

テーブル名、ヘッダー、データを順番に指定してあげて、
その後、write_tableを呼び出すとマークダウンのテキストを生成してくれますね。

カラム名は指定しないと、A,B,C,Dとアルファベットが割り当てられるようです。


writer = MarkdownTableWriter()
writer.value_matrix = [
    ["1", "AD", "Europe/Andorra"],
    ["2", "AE", "Asia/Dubai"],
    ["3", "AF", "Asia/Kabul"],
    ["4", "AG", "America/Antigua"],
    ["5", "AI", "America/Anguilla"],
]
writer.write_table()
"""
| A | B |       C        |
|--:|---|----------------|
|  1|AD |Europe/Andorra  |
|  2|AE |Asia/Dubai      |
|  3|AF |Asia/Kabul      |
|  4|AG |America/Antigua |
|  5|AI |America/Anguilla|
"""

引数で渡すのではなく、.value_matrixなどの変数にデータを渡すのが特徴的で、
ちょっと使い方に癖がありますが手軽にマークダウンを生成できるので便利ですね。