GridDB Pythonクライアントに新しい時系列関数を追加する

はじめに

GridDB Pythonクライアントの新バージョンがリリースされ、いくつかの新しい時系列関数が追加されました。これらの関数は Python クライアントの新機能ですが、このリリース以前から、GridDB のネイティブ言語 (java) や TQL 文字列、クエリで使用されています。

これらの関数は、同等のTQLと比較してよりシンプルに使用することができます。TQLクエリについては、こちらを参照してください。

このブログでは、この3つの関数の用途や使い方を説明し、kaggleから自由に利用できるデータセットを使っていくつかの例を紹介したいと思います。3つの関数とは、aggregate_time_seriesquery_by_time_series_range、そして query_by_time_series_sampling です。

また、Javaを使用してcsvファイルからデータを取り込む方法についても簡単に説明します。さらに、新しいPythonクライアントをビルドして実行するためのすべての手順を含むDockerfileについても紹介します。

Pythonクライアントのストール

最初に、GridDBをインストールします。インストール方法はこちらのdocsを参照してください。

GridDB Python Client github ページに記載されているCentOSの環境要件は以下の通りです。

OS: CentOS 7.6(x64) (GCC 4.8.5)
SWIG: 3.0.12
Python: 3.6
GridDB C client: V4.5 CE(Community Edition)
GridDB server: V4.5 CE, CentOS 7.6(x64) (GCC 4.8.5)

Dockerfile

今回用意した Dockerfile は、すべての prereq をビルド、メイクし、ファイルの一番下にあるPython スクリプトを実行します。

DockerfileをDockerhubから引っ張ってくると簡単に行えます。

docker pull griddbnet/python-client-v0.8.5:latest

Dockerfileのファイルの全体像は以下の通りです。

FROM centos:7

RUN yum -y groupinstall "Development Tools"
RUN yum -y install epel-release wget
RUN yum -y install pcre2-devel.x86_64
RUN yum -y install openssl-devel libffi-devel bzip2-devel -y
RUN yum -y install xz-devel  perl-core zlib-devel -y
RUN yum -y install numpy scipy

# Make c_client
WORKDIR /
RUN wget --no-check-certificate https://github.com/griddb/c_client/archive/refs/tags/v4.6.0.tar.gz
RUN tar -xzvf v4.6.0.tar.gz
WORKDIR /c_client-4.6.0/client/c
RUN  ./bootstrap.sh
RUN ./configure
RUN make
WORKDIR /c_client-4.6.0/bin
ENV LIBRARY_PATH ${LIBRARY_PATH}:/c_client-4.6.0/bin
ENV LD_LIBRARY_PATH ${LD_LIBRARY_PATH}:/c_client-4.6.0/bin

# Make SSL for Python3.10
WORKDIR /
RUN wget  --no-check-certificate https://www.openssl.org/source/openssl-1.1.1c.tar.gz
RUN tar -xzvf openssl-1.1.1c.tar.gz
WORKDIR /openssl-1.1.1c
RUN ./config --prefix=/usr --openssldir=/etc/ssl --libdir=lib no-shared zlib-dynamic
RUN make
RUN make test
RUN make install

# Build Python3.10
WORKDIR /
RUN wget https://www.python.org/ftp/python/3.10.4/Python-3.10.4.tgz
RUN tar xvf Python-3.10.4.tgz
WORKDIR /Python-3.10.4
RUN ./configure --enable-optimizations  -C --with-openssl=/usr --with-openssl-rpath=auto --prefix=/usr/local/python-3.version
RUN make install
ENV PATH ${PATH}:/usr/local/python-3.version/bin

python3 -m pip install pandasを実行します。

# Make Swig
WORKDIR /
RUN wget https://github.com/swig/swig/archive/refs/tags/v4.0.2.tar.gz
RUN tar xvfz v4.0.2.tar.gz
WORKDIR /swig-4.0.2
RUN chmod +x autogen.sh
RUN ./autogen.sh
RUN ./configure
RUN make
RUN make install
WORKDIR /

# Make Python Client
RUN wget https://github.com/griddb/python_client/archive/refs/tags/0.8.5.tar.gz
RUN tar xvf 0.8.5.tar.gz
WORKDIR /python_client-0.8.5
RUN make
ENV PYTHONPATH /python_client-0.8.5

WORKDIR /app

COPY time_series_example.py /app
ENTRYPOINT ["python3", "-u", "time_series_example.py"]

コンテナを使用せずにpythonクライアントをご自分のマシンにインストールする場合は、ファイルの説明書に記載されている手順に従ってください。

このコンテナを使用する場合、GridDB Serverをホストする第2のコンテナを実行するか、現在実行中のGridDBインスタンスを使用するかのいずれかを選択することができます。これは、dockerイメージの実行中に network フラグを使用することで実現できます。

docker run -it --network host --name python_client <image id>

データの取り込み

今回使用するデータセットは、kaggleのウェブサイトから無料でダウンロードでき、csv形式で提供されています。このデータをGridDBサーバに取り込むには、ネイティブコネクタであるjavaを使用しますが、pythonを使用して取り込むことも可能です。

データを取り込むためのjavaコードは、こちらのGithub Repoからダウンロードできます。

時系列の機能

このGridDBコネクタに追加された3つの関数( aggregate_time_seriesquery_by_time_series_rangequery_by_time_series_sampling )の便利な使い方はそれぞれ異なりますが、どの関数も、大規模なデータセットに対して統計的な洞察を得ることで、開発担当者やエンジニアが意味のある分析を行うのに役立ちます。

このブログの残りの部分では、各関数を一つずつ見ていき、私たちのデータセットに対して実行する様子を紹介し、なぜその関数が必要なのかを説明したいと思います。

まず、Javaで接続するのと同様の方法で、PythonでGridDBサーバに接続します。

#!/usr/bin/python

import griddb_python as griddb
import sys
import calendar
import datetime

factory = griddb.StoreFactory.get_instance()

#Get GridStore object
store = factory.get_store(
    host="239.0.0.1",
    port=31999,
    cluster_name="defaultCluster",
    username="admin",
    password="admin"
)

また、クエリを実行するために、新しく作成したデータセットを取得します。

ts = store.get_container("population")
query = ts.query("select * from population where value > 327000")
rs = query.fetch()

GridDBの時刻オブジェクトはUnix時間なので、1970年以前のデータセットを取り込もうとすると、ミリ秒の時刻が負数になってしまい、GridDBのエラーになります。今回はデモのため、このデータをデータセットから削除しました。

そこで、あまり多くの行を検索しないように、また1970年以前のデータの欠落を避けるために、人口が280000以上(これらの値は千単位です)から検索を開始することにします。ということは、1999年頃(20数年前)になります。

このクエリは20年分のデータを取得しますが、簡素化して、クエリパラメータを超えた最初の日付から単純に開始し、それ以降の時系列分析を使用することにします。

data = rs.next() #grabs just the first row from our entire query
timestamp = calendar.timegm(data[0].timetuple()) #data[0] is the timestamp
gsTS = (griddb.TimestampUtils.get_time_millis(timestamp)) #converts the data to millis
time = datetime.datetime.fromtimestamp(gsTS/1000.0) # converts back to a usable java datetime obj for the time series functions

時系列の集計

aggregation機能は少し独特で、他のクエリのように行のセットではなく、 AggregationResult を返します。これらの結果は、最小値、最大値、合計値、平均値、分散、標準偏差、個数、加重平均の値を取得することができます。

それでは、これらの例を見ていきましょう。まず、いくつかの予備変数を設定します。

data = rs.next()
year_in_mili = 31536000000 # this is one year in miliseconds
added = gsTS + (year_in_mili * 7) # 7 years after our start time (this is end time)
addedTime = datetime.datetime.fromtimestamp(added/1000.0) # converting to datetime obj as this is what the function expects

ここでは、クエリから返された最初の行を開始時刻とし、その7年後を終了時刻としていることがわかります。

つまり、集計タイプを min に設定すると、この関数はデータセットから最小の値(最小の数値)を返します。maxはその逆で、結果の中から最大の整数を返します。Totalは、すべての値の合計を返します。

AggregationResultが戻り値の型であり、期待されるパラメータは以下のようになります。aggregate_time_series(object start, object end, Aggregation type, string column_name=None) これは完全に形成されたものとなります。

total = ts.aggregate_time_series(time, addedTime, griddb.Aggregation.TOTAL, "value")
print("TOTAL: ", total.get(griddb.Type.LONG))

TOTAL: 48714984

代表値は平均値でもあり、単純にすべての値の総和をカウントで割ったものを取ります。

avg = ts.aggregate_time_series(time, addedTime, griddb.Aggregation.AVERAGE, "value")
print("AVERAGE: ", avg.get(griddb.Type.LONG))

AVERAGE: 289970

1999年から2006年までの平均は約2億9,000万でした。

分散とは、数学的には「平均値からの差の二乗の平均」と定義されます。これは本質的に、各数値が平均値・代表値からどれだけ異なっているかを意味します。

標準偏差は分散と似ていて、ある数字のグループが平均からどれくらい離れているかを見る統計的な測定方法です。簡単に言うと、標準偏差は、データセットの中で数字がどれだけ離れているかを測定するものです。通常、数値が平均値に対してどれだけ密接に関係しているかを分析するために使用されます。

最後に、加重平均について説明します。加重平均は、平均値におけるある値の重要性を定量化しようとするものです。時系列データの場合、一般的に2つのデータポイント間の時間空間を測定し、それを重み付けしようとします。このデータセットでは、各データポイントはちょうど1ヶ月間隔なので、残念ながらこのクエリで出力される数値は平均と同じになります。

weightedAvg = ts.aggregate_time_series(time, addedTime, griddb.Aggregation.WEIGHTED_AVERAGE, "value")
print("WEIGHTED AVERAGE: ", weightedAvg.get(griddb.Type.LONG))

WEIGHTED AVERAGE: 289970

しかし、時系列データが不規則な間隔であった場合、生成される数値は我々の平均とは異なるものになったはずです。これについては、以前のブログに詳しく書かれています。

時系列範囲のクエリ

時系列の範囲に関するクエリは、他の多くのクエリと同じように、行型のセットを返します。この関数は以下のようなものです。query_by_time_series_range(object start, object end, QueryOrder order=QueryOrder.ASCENDING) これは、開始時刻から終了時刻までの行を返します。つまり、開発担当者は明示的な時間範囲を取得する簡単な方法を得ることができます。

具体的な例を挙げてみましょう。

rangeQuery = ts.query_by_time_series_range(time, addedTime, griddb.QueryOrder.ASCENDING)
rangeRs = rangeQuery.fetch()
while rangeRs.has_next():
    d = rangeRs.next()
    print("d: ", d)

結果は、単純に範囲です。

d:  [datetime.datetime(1999, 10, 1, 0, 0), 280203]
d:  [datetime.datetime(1999, 10, 1, 7, 0), 280203]
d:  [datetime.datetime(1999, 11, 1, 0, 0), 280471]
d:  [datetime.datetime(1999, 11, 1, 8, 0), 280471]
d:  [datetime.datetime(1999, 12, 1, 0, 0), 280716]
d:  [datetime.datetime(1999, 12, 1, 8, 0), 280716]
d:  [datetime.datetime(2000, 1, 1, 0, 0), 280976]
d:  [datetime.datetime(2000, 1, 1, 8, 0), 280976]
d:  [datetime.datetime(2000, 2, 1, 0, 0), 281190]
d:  [datetime.datetime(2000, 2, 1, 8, 0), 281190]
d:  [datetime.datetime(2000, 3, 1, 0, 0), 281409]
d:  [datetime.datetime(2000, 3, 1, 8, 0), 281409]
d:  [datetime.datetime(2000, 4, 1, 0, 0), 281653]
d:  [datetime.datetime(2000, 4, 1, 8, 0), 281653]
d:  [datetime.datetime(2000, 5, 1, 0, 0), 281877]
d:  [datetime.datetime(2000, 5, 1, 7, 0), 281877]
d:  [datetime.datetime(2000, 6, 1, 0, 0), 282126]
d:  [datetime.datetime(2000, 6, 1, 7, 0), 282126]
d:  [datetime.datetime(2000, 7, 1, 0, 0), 282385]
d:  [datetime.datetime(2000, 7, 1, 7, 0), 282385]
d:  [datetime.datetime(2000, 8, 1, 0, 0), 282653]
d:  [datetime.datetime(2000, 8, 1, 7, 0), 282653]
d:  [datetime.datetime(2000, 9, 1, 0, 0), 282932]
d:  [datetime.datetime(2000, 9, 1, 7, 0), 282932]

時系列サンプリングによるクエリ

新しい関数の中で私が個人的に最も面白いと思うのは、最後のものです。このサンプリングは、各ポイントの間に設定された時間を持つ行の一様なサンプルを返します。

時系列サンプリング関数も行の集合を返します。この関数は、すべての関数の中で最も多くのパラメータを取ります。query_by_time_series_sampling(object start, object end, list[string] column_name_list, InterpolationMode mode, int interval, TimeUnit interval_unit)` これも開始と終了を受け取りますが、今回は列名のリストを受け取り、さらに補間モードと間隔、時間単位を受け取ります。

補間モードは、LINEAR_OR_PREVIOUS または EMPTYのいずれかを選択することができます。インターバルの単位には、以下の選択肢があります。年、月、日、時、分、秒、ミリ秒  ただし、年や月は大きすぎて間隔として認められないので、基本的には日以下の間隔を選択することになります。

ここでは、ブログのために行った具体例を紹介します。

try:   
    samplingQuery = ts.query_by_time_series_sampling(time, addedTime, ["value"], griddb.InterpolationMode.LINEAR_OR_PREVIOUS, 1, griddb.TimeUnit.DAY) # the columns need to be a list, hence the [ ]
    samplingRs = samplingQuery.fetch()
    while samplingRs.has_next(): 
        d = samplingRs.next()
        print("sampling: ", d)
except griddb.GSException as e:
    for i in range(e.get_error_stack_size()):
        print("[", i, "]")
        print(e.get_error_code(i))
        print(e.get_message(i))

addedtime変数は、最初の7年後ですが、ここでは、出力されたクエリのほんの一端を紹介します。

sampling:  [datetime.datetime(2006, 8, 1, 0, 0), 299263]
sampling:  [datetime.datetime(2006, 8, 2, 0, 0), 299269]
sampling:  [datetime.datetime(2006, 8, 3, 0, 0), 299279]
sampling:  [datetime.datetime(2006, 8, 4, 0, 0), 299288]
sampling:  [datetime.datetime(2006, 8, 5, 0, 0), 299298]
sampling:  [datetime.datetime(2006, 8, 6, 0, 0), 299307]
sampling:  [datetime.datetime(2006, 8, 7, 0, 0), 299317]
sampling:  [datetime.datetime(2006, 8, 8, 0, 0), 299326]
sampling:  [datetime.datetime(2006, 8, 9, 0, 0), 299336]
sampling:  [datetime.datetime(2006, 8, 10, 0, 0), 299345]
sampling:  [datetime.datetime(2006, 8, 11, 0, 0), 299354]
sampling:  [datetime.datetime(2006, 8, 12, 0, 0), 299364]
sampling:  [datetime.datetime(2006, 8, 13, 0, 0), 299373]
sampling:  [datetime.datetime(2006, 8, 14, 0, 0), 299383]
sampling:  [datetime.datetime(2006, 8, 15, 0, 0), 299392]
sampling:  [datetime.datetime(2006, 8, 16, 0, 0), 299402]
sampling:  [datetime.datetime(2006, 8, 17, 0, 0), 299411]
sampling:  [datetime.datetime(2006, 8, 18, 0, 0), 299421]
sampling:  [datetime.datetime(2006, 8, 19, 0, 0), 299430]
sampling:  [datetime.datetime(2006, 8, 20, 0, 0), 299440]
sampling:  [datetime.datetime(2006, 8, 21, 0, 0), 299449]
sampling:  [datetime.datetime(2006, 8, 22, 0, 0), 299459]
sampling:  [datetime.datetime(2006, 8, 23, 0, 0), 299468]
sampling:  [datetime.datetime(2006, 8, 24, 0, 0), 299478]
sampling:  [datetime.datetime(2006, 8, 25, 0, 0), 299487]
sampling:  [datetime.datetime(2006, 8, 26, 0, 0), 299497]
sampling:  [datetime.datetime(2006, 8, 27, 0, 0), 299506]
sampling:  [datetime.datetime(2006, 8, 28, 0, 0), 299516]
sampling:  [datetime.datetime(2006, 8, 29, 0, 0), 299525]
sampling:  [datetime.datetime(2006, 8, 30, 0, 0), 299535]
sampling:  [datetime.datetime(2006, 8, 31, 0, 0), 299544]
sampling:  [datetime.datetime(2006, 9, 1, 0, 0), 299554]
sampling:  [datetime.datetime(2006, 9, 2, 0, 0), 299560]
sampling:  [datetime.datetime(2006, 9, 3, 0, 0), 299570]
sampling:  [datetime.datetime(2006, 9, 4, 0, 0), 299579]
sampling:  [datetime.datetime(2006, 9, 5, 0, 0), 299589]
sampling:  [datetime.datetime(2006, 9, 6, 0, 0), 299598]
sampling:  [datetime.datetime(2006, 9, 7, 0, 0), 299607]
sampling:  [datetime.datetime(2006, 9, 8, 0, 0), 299617]
sampling:  [datetime.datetime(2006, 9, 9, 0, 0), 299626]
sampling:  [datetime.datetime(2006, 9, 10, 0, 0), 299636]

元のデータセットでは、毎月1日の母数を得ることができます。また、サンプリングにより、1日単位で母集団の値を推定することができます。このデータから、kaggleから得た値は正しく、その日に至るまでの母集団は妥当であることがわかります。例えば、2006年9月1日の母集団の値は299554で、これはkaggleのデータと一致し、その前日の値も299544です。

まとめ

このブログでは、Docker を使って、あるいは Dockerfile のステップバイステップの指示に従って、新しい Python クライアントを構築する方法を紹介しました。また、csvファイルからの非常にシンプルなデータインジェストも(java経由で)実演しました。そして最後に、新しい時系列関数が時系列分析にどれだけ有用であるかを示しました。

このブログの全コードはこちらからダウンロードできます。

ブログの内容について疑問や質問がある場合は Q&A サイトである Stack Overflow に質問を投稿しましょう。 GridDB 開発者やエンジニアから速やかな回答が得られるようにするためにも "griddb" タグをつけることをお忘れなく。 https://stackoverflow.com/questions/ask?tags=griddb

Leave a Reply

Your email address will not be published. Required fields are marked *