時系列データをMatplotlibで描画する

時系列データを描画したい。パターンとして以下の2つを考える。

  • 株価
    5分刻みで取得した株価データを描画するなど。
  • アクセス数
    nginxのアクセスログのデータから各時間帯でのアクセス数をヒストグラムにするなど。

この記事ではこれらそのものはやらないが、もう少し単純な例で描画してみる。

やりたいこと

  • (時刻, 値)の組の形の時系列データを描画する
    株価など。
  • 大量のタイムスタンプをヒストグラムとして描画する
    アクセス数など。

使うもの

  • Python
    3.7.6で確認。
  • Matplotlib
    3.1.3で確認。
  • pandas
    0.25.3で確認。
    pandasはデータ格納用に使うだけ。
    なおpandas+Matplotlibでdatetimeを扱うときは以下を実行しないと警告が出るようだ。

    from pandas.plotting import register_matplotlib_converters
    register_matplotlib_converters()
    

方法

目盛の設定

例1: (時刻, 値)の組の形の時系列データを描画する

import

import matplotlib.pyplot as plt
from matplotlib import dates as mdates
import pandas as pd
from pandas.plotting import register_matplotlib_converters

データ

0時に0、そこから増加して12時に最大(12)になり、
あとは減少して0時に再び0に戻るというサンプルデータを用意する。
DataFrameに格納しておく。

data = [
    ['2020-01-01 00:00:00', 0],
    ['2020-01-01 01:00:00', 1],
    ['2020-01-01 02:00:00', 2],
    ['2020-01-01 03:00:00', 3],
    ['2020-01-01 04:00:00', 4],
    ['2020-01-01 05:00:00', 5],
    ['2020-01-01 06:00:00', 6],
    ['2020-01-01 07:00:00', 7],
    ['2020-01-01 08:00:00', 8],
    ['2020-01-01 09:00:00', 9],
    ['2020-01-01 10:00:00', 10],
    ['2020-01-01 11:00:00', 11],
    ['2020-01-01 12:00:00', 12],
    ['2020-01-01 13:00:00', 11],
    ['2020-01-01 14:00:00', 10],
    ['2020-01-01 15:00:00', 9],
    ['2020-01-01 16:00:00', 8],
    ['2020-01-01 17:00:00', 7],
    ['2020-01-01 18:00:00', 6],
    ['2020-01-01 19:00:00', 5],
    ['2020-01-01 20:00:00', 4],
    ['2020-01-01 21:00:00', 3],
    ['2020-01-01 22:00:00', 2],
    ['2020-01-01 23:00:00', 1],
    ['2020-01-02 00:00:00', 0],
    ['2020-01-02 01:00:00', 1],
    ['2020-01-02 02:00:00', 2],
    ['2020-01-02 03:00:00', 3],
    ['2020-01-02 04:00:00', 4],
    ['2020-01-02 05:00:00', 5],
    ['2020-01-02 06:00:00', 6],
    ['2020-01-02 07:00:00', 7],
    ['2020-01-02 08:00:00', 8],
    ['2020-01-02 09:00:00', 9],
    ['2020-01-02 10:00:00', 10],
    ['2020-01-02 11:00:00', 11],
    ['2020-01-02 12:00:00', 12],
    ['2020-01-02 13:00:00', 11],
    ['2020-01-02 14:00:00', 10],
    ['2020-01-02 15:00:00', 9],
    ['2020-01-02 16:00:00', 8],
    ['2020-01-02 17:00:00', 7],
    ['2020-01-02 18:00:00', 6],
    ['2020-01-02 19:00:00', 5],
    ['2020-01-02 20:00:00', 4],
    ['2020-01-02 21:00:00', 3],
    ['2020-01-02 22:00:00', 2],
    ['2020-01-02 23:00:00', 1],
    ['2020-01-03 00:00:00', 0],
    ['2020-01-03 01:00:00', 1],
    ['2020-01-03 02:00:00', 2],
    ['2020-01-03 03:00:00', 3],
    ['2020-01-03 04:00:00', 4],
    ['2020-01-03 05:00:00', 5],
    ['2020-01-03 06:00:00', 6],
    ['2020-01-03 07:00:00', 7],
    ['2020-01-03 08:00:00', 8],
    ['2020-01-03 09:00:00', 9],
    ['2020-01-03 10:00:00', 10],
    ['2020-01-03 11:00:00', 11],
    ['2020-01-03 12:00:00', 12],
    ['2020-01-03 13:00:00', 11],
    ['2020-01-03 14:00:00', 10],
    ['2020-01-03 15:00:00', 9],
    ['2020-01-03 16:00:00', 8],
    ['2020-01-03 17:00:00', 7],
    ['2020-01-03 18:00:00', 6],
    ['2020-01-03 19:00:00', 5],
    ['2020-01-03 20:00:00', 4],
    ['2020-01-03 21:00:00', 3],
    ['2020-01-03 22:00:00', 2],
    ['2020-01-03 23:00:00', 1],
]

df = pd.DataFrame(data, columns=['datetime', 'val'])

プロット

# pandasでdatetime扱うため
register_matplotlib_converters()

# フォーマット定義
days = mdates.DayLocator()
hours = mdates.HourLocator()
day_fmt = mdates.DateFormatter('%Y/%m/%d')

# グラフ定義
fig, ax = plt.subplots()
x = pd.to_datetime(df['datetime'], format='%Y-%m-%d %H:%M:%S').to_list()
y = df['val'].to_list()
ax.plot(x, y)
ax.grid()

# フォーマット設定
ax.xaxis.set_major_locator(days)
ax.xaxis.set_minor_locator(hours)
ax.xaxis.set_major_formatter(day_fmt)

# 描画範囲の指定
datetime_min, datetime_max = min(x), max(x)
ax.set_xlim(datetime_min, datetime_max)

# x軸のラベルの回転
labels = ax.get_xticklabels()
plt.setp(labels, rotation=45, fontsize=10)

plt.show()

例2: 大量のタイムスタンプをヒストグラムとして描画する

import

import matplotlib.pyplot as plt
from matplotlib import dates as mdates
import pandas as pd
import datetime as dt
import random
from pandas.plotting import register_matplotlib_converters

データ

ヒストグラムを描きたいので、わりと大量にデータを用意する必要がある。
ここでは乱数を用いて、1週間ぶんのタイムスタンプを1000個生成する。
(もっといい方法があるのかもしれないが…。)

# 作成するdatetime数
N = 1000

datetime_from, datetime_to = dt.datetime(2020, 1, 1, 0, 0, 0), dt.datetime(2020, 1, 8, 0, 0, 0)
timestamp_from, timestamp_to = int(datetime_from.timestamp()), int(datetime_to.timestamp())
random_timestamps = [random.randint(timestamp_from, timestamp_to) for _ in range(0, N)]
random_datetimes = [dt.datetime.fromtimestamp(t) for t in random_timestamps]
random_datetimes_str = [dt.strftime("%Y-%m-%d %H:%M:%S") for dt in random_datetimes]

生成したデータはこんなふうになる。

# random_datetimes_str[:10]の結果

['2020-01-06 07:14:19',
 '2020-01-01 13:39:55',
 '2020-01-01 01:37:01',
 '2020-01-02 13:14:58',
 '2020-01-02 22:22:01',
 '2020-01-06 08:08:00',
 '2020-01-05 08:53:54',
 '2020-01-07 03:05:51',
 '2020-01-05 09:19:09',
 '2020-01-04 01:46:38']

プロット

# pandasでdatetime扱うため
register_matplotlib_converters()

# フォーマット定義
days = mdates.DayLocator()
hours = mdates.HourLocator()
day_fmt = mdates.DateFormatter('%Y/%m/%d')

# グラフ定義
fig, ax = plt.subplots()
x = [dt.datetime.strptime(s, "%Y-%m-%d %H:%M:%S") for s in random_datetimes_str]
ax.hist(x, bins=7)

# フォーマット設定
ax.xaxis.set_major_locator(days)
ax.xaxis.set_major_formatter(day_fmt)
ax.xaxis.set_minor_locator(hours)

# 描画範囲の指定
datemin, datemax = min(x), max(x)
ax.set_xlim(datemin, datemax)

ax.grid()

# x軸のラベルの回転
labels = ax.get_xticklabels()
plt.setp(labels, rotation=45, fontsize=10)

plt.show()

参考

pre-commitでPythonの開発環境を整備する

Pythonを使った開発をしているとき、コードレビューでコーディングスタイルなどに関する表面的な指摘をするのが面倒だ。効率化するために、コミットの時点でこれらについてのチェックを自動で行い、エラーになった場合はコミットできないようにする。

はじめGit標準のpre-commit hookでやろうと思っていたが、pre-commitの方が設定や管理が楽そうなので、これを選んだ。

やりたいこと

コミット時に以下のチェックを行い、NGならコミットできなくする

  • オートフォーマット
    autopep8を使う。
  • コーディングスタイルのチェック
    flake8を使う。
  • 型ヒントのチェック
    mypyを使う。

pre-commitとは

Git標準のpre-commit hookを管理するためのツール。設定はyaml管理で書ける。
言語としてはPython以外にGolang、Rustなどもサポートしている

方法

  1. pre-commitのインストール
    pip3 install pre-commit
    
  2. 対象のローカルリポジトリのルートにcd
    当然だが、git管理されているリポジトリでないといけない。
  3. .pre-commit-config.yaml の作成・編集
    .pre-commit-config.yaml という名前でファイルを作り、以下のように書く。なお rev はその repo で使いたいリビジョンやタグを書く。

    -   repo: https://github.com/pre-commit/mirrors-autopep8
        rev: v1.5
        hooks:
        -   id: autopep8
    -   repo: https://gitlab.com/pycqa/flake8
        rev: 3.7.9
        hooks:
        -   id: flake8
    -   repo: https://github.com/pre-commit/mirrors-mypy
        rev: v0.761
        hooks:
        -   id: mypy
            args: [--ignore-missing-imports]
    
  4. Gitのpre-commit hookの設定
    pre-commit install
    

    (この時点では .pre-commit-config.yaml に書いた各種ツールはまだダウンロードしていない。単にそのリポジトリの .git 以下にhookスクリプトが入るだけ。ダウンロードするのは次回のコミット時。)

これで設定完了。あとはいつも通り、ファイル修正して git addgit commit すればいい。設定したチェックが走り、エラーがあればコミットができなくなる。

なお以下のコマンドならコミットのタイミング以外でもチェックできる。

pre-commit run --all-files

試す

以下のような問題のあるコード(app.py)をコミットしようとした。

msg:int="Hello, world!"
print( msg )

チェックされて、次のようなエラー。コミットできない!(flake8によるチェックが通っているのはautopep8が修正したため。)

$ git commit
autopep8.................................................................Failed
- hook id: autopep8
- files were modified by this hook
flake8...................................................................Passed
mypy.....................................................................Failed
- hook id: mypy
- exit code: 1
- files were modified by this hook

app.py:1: error: Incompatible types in assignment (expression has type "str", variable has type "int")
Found 1 error in 1 file (checked 1 source file)

以下のように修正して再コミット。(フォーマットはautopep8がやってくれたので、型ヒントだけ修正すればいい。)

msg: str = "Hello, world!"
print(msg)

これで通る。

$ git commit
autopep8.................................................................Passed
flake8...................................................................Passed
mypy.....................................................................Passed

あとはいつもと同じ。

補足

.pre-commit-config.yaml に書いたautopep8とかはどこに入ったのだろうと探したら ~/.cache/pre-commit 以下に入っていた。これはpre_commit/store.pyで設定しているよう。

参考

MeCab+NEologdを使って日本語を分かち書きにするツールを作った

Word2VecとかするときにMeCab + NEologdを使って分かち書きする。だがインストールがやや面倒。Dockerを使って、手軽に分かち書きするツールを作った。

やりたいこと

  1. MeCab + NEologdで分かち書きする
  2. ディレクトリ構造を保つ
    Word2Vecで全テキストを1つにまとめてしまうときは問題ないが、Doc2Vecとかするときには1ファイルごとに分かち書きしたい。そのときディレクトリ構造を変えられると面倒なので、同じになるようにする。
    たとえば以下のような感じ。inputに分かち書きしたいファイルをディレクトリごと入れたら、その構造を保ったままoutputに結果が出る。

    ├── input
    │   ├── 1
    │   │   ├── 11
    │   │   │   └── c.txt
    │   │   └── b.txt
    │   ├── 2
    │   │   └── d.txt
    │   └── a.txt
    └── output
       ├── 1
       │   ├── 11
       │   │   └── c.txt
       │   └── b.txt
       ├── 2
       │   └── d.txt
       └── a.txt
    

できたもの

sankaku/docker-mecab

実行例

./mount/input に上のツリー構造の通りファイルを入れる。中身はすべて同じで、NEologdにある例と同じにした。
そしてDockerイメージを作成し、 ./split.sh を実行。

入力

$ for f in `ls mount/input/**/*.txt`; do echo $f; cat $f; done
mount/input/1/11/c.txt
8月3日に放送された「中居正広の金曜日のスマイルたちへ」(TBS系)で、1日たった5分でぽっこりおなかを解消するというダイエット方法を紹介。キンタロー。のダイエットにも密着。
mount/input/1/b.txt
8月3日に放送された「中居正広の金曜日のスマイルたちへ」(TBS系)で、1日たった5分でぽっこりおなかを解消するというダイエット方法を紹介。キンタロー。のダイエットにも密着。
mount/input/2/d.txt
8月3日に放送された「中居正広の金曜日のスマイルたちへ」(TBS系)で、1日たった5分でぽっこりおなかを解消するというダイエット方法を紹介。キンタロー。のダイエットにも密着。
mount/input/a.txt
8月3日に放送された「中居正広の金曜日のスマイルたちへ」(TBS系)で、1日たった5分でぽっこりおなかを解消するというダイエット方法を紹介。キンタロー。のダイエットにも密着。

出力

$ for f in `ls mount/output/**/*.txt`; do echo $f; cat $f; done
mount/output/1/11/c.txt
8月3日 に 放送 さ れ た 「 中居正広の金曜日のスマイルたちへ 」( TBS 系 ) で 、 1日 たった 5分 で ぽっこり おなか を 解消 する という ダイエット方法 を 紹介 。 キンタロー。 の ダイエット に も 密着 。 
mount/output/1/b.txt
8月3日 に 放送 さ れ た 「 中居正広の金曜日のスマイルたちへ 」( TBS 系 ) で 、 1日 たった 5分 で ぽっこり おなか を 解消 する という ダイエット方法 を 紹介 。 キンタロー。 の ダイエット に も 密着 。 
mount/output/2/d.txt
8月3日 に 放送 さ れ た 「 中居正広の金曜日のスマイルたちへ 」( TBS 系 ) で 、 1日 たった 5分 で ぽっこり おなか を 解消 する という ダイエット方法 を 紹介 。 キンタロー。 の ダイエット に も 密着 。 
mount/output/a.txt
8月3日 に 放送 さ れ た 「 中居正広の金曜日のスマイルたちへ 」( TBS 系 ) で 、 1日 たった 5分 で ぽっこり おなか を 解消 する という ダイエット方法 を 紹介 。 キンタロー。 の ダイエット に も 密着 。

ちゃんとファイルごとに分かち書きできてる。
ディレクトリ構造もそのまま。


$ tree mount/ mount/ ├── input │   ├── 1 │   │   ├── 11 │   │   │   └── c.txt │   │   └── b.txt │   ├── 2 │   │   └── d.txt │   └── a.txt └── output ├── 1 │   ├── 11 │   │   └── c.txt │   └── b.txt ├── 2 │   └── d.txt └── a.txt