ヒトリ歩き

愚痴とかいろいろ書きます

HTTPリクエストの同期/非同期の処理動作

HTTPリクエストの同期/非同期の動きについて確認する。より、多くのリクエストをより早く処理するためには非同期の存在は不可欠だと考える。

なぜ、非同期が必要なのか?

リクエストの応答待ちをしている間に他の処理を動かして、効率よく処理をしたいから。
リクエストの応答がなかなか返ってこない間、他の処理は止まってしまう。その間にリクエストを受信してしまうと サーバー側は処理しきれなくなってしまう。
そのため、リクエストの応答を待っている間にリクエストの受信して他のリクエストを処理できるようにするために 非同期での実行が必要になる。

aiohttpとは

非同期で動作するHTTPクライアント/サーバー。
asyncioを使って動作する。 HTTPリクエストのモジュールでは、requestsがメジャーだと思う。
自分も仕事ではrequestsばかり使っている。

aiohttpのインストール

aiohttpモジュールをインストールするだけ。

pip install aiohttp

ハローワールド

GETリクエストのハローワールド。aiohttpのページに書かれているのawaitを使うように 少し変更。

import aiohttp
import asyncio


async def main():
    async with aiohttp.ClientSession() as session:
        resp = await session.get("http://httpbin.org/get")
        print(resp.status)
        print(await resp.text())


asyncio.run(main())

同期処理と非同期でのリクエスト受信時の動作確認

ここでは、同期で別のマイクロサービスにリクエストを送信する場合と非同期でリクエストを送信した場合の リクエストの処理の違いを確認する。 Webフレームワークは、FastAPIを使用する。

同期処理で別のマイクロサービスにリクエストを送信

リクエストを送信した際に、受信側が別のマイクロサービスに同期でリクエストを送信するケースを実験する。
送信元は、task1とtask2をほぼ同時にリクエストを送信しているが、リクエストはtask1の応答が返ってきて その後にtask2の応答結果が返ってきている。
これは、リクエストを同期で処理しているため、task1から順番に処理したことがわかる。

# 送信元
2024-04-20 07:59:04,569 - --- task1 -----
2024-04-20 07:59:04,569 - --- task1 Send!!
2024-04-20 07:59:04,570 - --- task2 -----
2024-04-20 07:59:04,570 - --- task2 Send!!
2024-04-20 07:59:14,606 - --- task1 status = 200
2024-04-20 07:59:14,606 - --- task1{"res":{"message":"success"}}
2024-04-20 07:59:24,612 - --- task2 status = 200
2024-04-20 07:59:24,612 - --- task2{"res":{"message":"success"}}

# 受信側
2024-04-20 07:59:04,572 - Received task1 Request!!
2024-04-20 07:59:14,604 - Send task1 response
INFO:     127.0.0.1:52673 - "GET /task1 HTTP/1.1" 200 OK
2024-04-20 07:59:14,606 - Received task2 Request!!
2024-04-20 07:59:24,611 - Send task2 response
INFO:     127.0.0.1:52674 - "GET /task2 HTTP/1.1" 200 OK

非同期で別のマイクロサービスにリクエストを送信

リクエストを非同期で処理したケースでは、さきほどと同様に送信元はtask1とtask2のリクエストを ほぼ同時に送信している。
リクエストを受信した側も同様にほぼ同時にリクエストを受信しており、応答結果もほぼ同時に返却している ことがわかる。

# 送信元
2024-04-20 08:01:19,726 - --- task1 -----
2024-04-20 08:01:19,726 - --- task1 Send!!
2024-04-20 08:01:19,726 - --- task2 -----
2024-04-20 08:01:19,726 - --- task2 Send!!
2024-04-20 08:01:29,740 - --- task1 status = 200
2024-04-20 08:01:29,740 - --- task1{"res":{"message":"success"}}
2024-04-20 08:01:29,740 - --- task2 status = 200
2024-04-20 08:01:29,740 - --- task2{"res":{"message":"success"}}

# 受信側
2024-04-20 08:01:19,728 - Received task1 Request!!
2024-04-20 08:01:19,732 - Received task2 Request!!
2024-04-20 08:01:29,738 - Send task1 response
INFO:     127.0.0.1:53064 - "GET /task1 HTTP/1.1" 200 OK
2024-04-20 08:01:29,739 - Send task2 response
INFO:     127.0.0.1:53065 - "GET /task2 HTTP/1.1" 200 OK

まとめ

aiohttpとrequestsモジュールを使って、同期と非同期時にHTTPリクエストの処理の仕方を 確認した。実際にプログラムを動かしてみることで、送信元と受信側の動きを理解することができた。
また、aiohttpの存在は知らなかったので、知ることが出来てよかった。
Postリクエストはデータの登録や更新、削除で使用されるため、同期処理した方が良さそうだが、 Getリクエストは非同期でより多くのリクエストを処理できるようにした方がよいと思う。
実際にプロジェクトの仕様にもよるが、aiohttpが使えることは頭に入れておこうと思う。

参考

  • 送信元
import aiohttp
import asyncio
import logging
from logging import getLogger, StreamHandler, Formatter

logger = getLogger("LogTest")
logger.setLevel(logging.DEBUG)
stream_handler = StreamHandler()
stream_handler.setLevel(logging.DEBUG)
handler_format = Formatter("%(asctime)s - %(message)s")
stream_handler.setFormatter(handler_format)
logger.addHandler(stream_handler)


async def send(name):
    logger.info("--- " + name + " -----")
    async with aiohttp.ClientSession() as session:
        logger.info("--- " + name + " Send!!")
        url = "http://127.0.0.1:8001/" + str(name)
        resp = await session.get(url)
        logger.info("--- " + name + " status = " + str(resp.status))
        logger.info("--- " + name + str(await resp.text()))


async def main():
    task1 = asyncio.create_task(send("task1"))
    task2 = asyncio.create_task(send("task2"))
    await task1
    await task2


asyncio.run(main())
  • app

uvicorn received_app:app --port 8001 で起動

from fastapi import FastAPI
import requests
import aiohttp
import logging
from logging import getLogger, StreamHandler, Formatter

logger = getLogger("LogTest")
logger.setLevel(logging.DEBUG)
stream_handler = StreamHandler()
stream_handler.setLevel(logging.DEBUG)
handler_format = Formatter("%(asctime)s - %(message)s")
stream_handler.setFormatter(handler_format)
logger.addHandler(stream_handler)

app = FastAPI()


@app.get("/task2")
async def get_task2():

    logger.info("Received task2 Request!!")
    # response = requests.get("http://127.0.0.1:8000")
    # logger.info("Send task2 response")
    # return {"res": response.json()}
    async with aiohttp.ClientSession() as session:
        response = await session.get("http://127.0.0.1:8000")
        json_res = await response.json()
        logger.info("Send task2 response")
        return {"res": json_res}


@app.get("/task1")
async def get_task1():

    logger.info("Received task1 Request!!")
    # response = requests.get("http://127.0.0.1:8000")
    # logger.info("Send task1 response")
    # return {"res": response.json()}
    async with aiohttp.ClientSession() as session:
        response = await session.get("http://127.0.0.1:8000")
        json_res = await response.json()
        logger.info("Send task1 response")
        return {"res": json_res}
  • app2

uvicorn late_app:app --port 8000 で起動

from fastapi import FastAPI
import time
import logging
from logging import getLogger, StreamHandler, Formatter

logger = getLogger("LogTest")
logger.setLevel(logging.DEBUG)
stream_handler = StreamHandler()
stream_handler.setLevel(logging.DEBUG)
handler_format = Formatter("%(asctime)s - %(message)s")
stream_handler.setFormatter(handler_format)
logger.addHandler(stream_handler)

app = FastAPI()


@app.get("/")
def call_api():
    logger.info("Received request in late app.")
    time.sleep(10)
    response = {"message": "success"}
    return response

RabbitMQでのメッセージ消失の対策

前々回にRabbitMQを触っていた続き。 少しだけQueueのところに触れておく。

kotapontan.hatenablog.com

www.rabbitmq.com

メッセージの処理中の考慮

タスクの処理中にワーカー側が終了した場合、タスクを別のワーカーに処理してもらいたい。 そのため、RabbitMQではメッセージの応答確認をサポートしている。 ackは、メッセージを受信し、処理後にRabbitMQが削除できることをRabbitMQに伝えるため、コンシューマーから通知する。 ackを送信せずに、コンシューマーが終了した場合、RabbitMQはメッセージが処理されなかったと認識して、メッセージを再度キューに入れる。 配信確認には、タイムアウト(デフォルト30分)となっている。

RabbitMQにメッセージを削除してよい通知をする場合は、コールバック関数の中で、channel.basic_ackを実行する。

試しに、basic_ackをコメントアウトして実行したところ、ワーカーはキューからメッセージを取得したが、キューにはメッセージが残った状態となった。

def callback(channel, method, properties, message):
    print(f"  [x] Received {message.decode()}")
    time.sleep(message.count(b"."))
    print("   [x] Done")
    channel.basic_ack(delivery_tag=method.delivery_tag) # ここをコメントアウト

メッセージの永続化

キューにメッセージがある状態で停止すると、キューのメッセージが喪失する。 そのような状況を防ぐためには、2つのことが必要。 1つ目は、quere_declare関数のパラメータにdurable=Trueを設定する。

channel.queue_declare(queue="hello", durable=True)

2つ目は、channel.basic_publishのpropertiesに永続化するプロパティを指定すること。

    channel.basic_publish(
        exchange="",
        routing_key="hello",
        body=message,
        properties=pika.BasicProperties(delivery_mode=pika.DeliveryMode.Persistent),
    )

注意が必要なのは、コンテナの再起動ではキューやメッセージは永続化されているが、コンテナを停止すると キューとメッセージは消えてしまう。

メッセージのディスパッチ

ジーではないワーカーにメッセージを割り振るには、channel.basic_qos(prefetch_count=1) を設定する。

試しにメッセージを受け取って、短いスリープをするワーカーと長いスリープをするワーカーに分けて実行してみた。 `channel.basic_qos(prefetch_count=1) を含めてない場合、均等にメッセージが割り振られているのがわかる。

# sleep 1
  [x] Received Message No.0
   [x] Done
  [x] Received Message No.2
   [x] Done
  [x] Received Message No.4
   [x] Done
  [x] Received Message No.6
   [x] Done
  [x] Received Message No.8
   [x] Done
  [x] Received Message No.10
   [x] Done
  [x] Received Message No.12
   [x] Done

# sleep 5
  [x] Received Message No.3
   [x] Done
  [x] Received Message No.5
   [x] Done
  [x] Received Message No.7
   [x] Done
  [x] Received Message No.9
   [x] Done
  [x] Received Message No.11
   [x] Done
  [x] Received Message No.13
   [x] Done
  [x] Received Message No.15
   [x] Done

次にchannel.basic_qos(prefetch_count=1)を設定したケース。 スリープが短いワーカーの処理が終わり次第、次のメッセージを処理していることがわかる。

# sleep 1
  [x] Received Message No.1
   [x] Done
  [x] Received Message No.3
   [x] Done
  [x] Received Message No.4
   [x] Done
  [x] Received Message No.5
   [x] Done
  [x] Received Message No.6
   [x] Done
  [x] Received Message No.7
   [x] Done
  [x] Received Message No.9
   [x] Done
  [x] Received Message No.10
   [x] Done
  [x] Received Message No.11
   [x] Done
  [x] Received Message No.12
   [x] Done
  [x] Received Message No.13
   [x] Done
  [x] Received Message No.15
   [x] Done
  [x] Received Message No.16
   [x] Done
  [x] Received Message No.17
   [x] Done
  [x] Received Message No.18
   [x] Done
  [x] Received Message No.20
   [x] Done
  [x] Received Message No.21
   [x] Done

# sleep 5
  [x] Received Message No.0
   [x] Done
  [x] Received Message No.2
   [x] Done
  [x] Received Message No.8
   [x] Done
  [x] Received Message No.14
   [x] Done
  [x] Received Message No.19
   [x] Done

最後に

メッセージの処理中にワーカー側が停止した場合やRabbitMQ側が停止した場合などのメッセージの消失に関して考慮されていた。 ただ、コンテナで起動している場合はメッセージが消失していたので、別のやり方があるのか、自分の確認が悪かったのかはどこかで確認してみる。

2024年3月の振り返り

あっという間に3月も終わりなので、3月の振り返り。

kotapontan.hatenablog.com

やったこと

朝活または夜活を20分やる

3月は出社が多く、3月の始めは帰宅して夜活をやっていましたが、睡眠不足や疲れを感じ始めた。 そのため、出社する前にやるように変更した。 出社するのが30分ほど遅くなったけど、無理なく続けられている。

投稿をさらにいつもより頑張ってみた

2月が10記事の投稿。3月は14記事を投稿。 2月よりも4記事多く投稿できた。PV数は2510PVだった。 pandasの記事を投稿した日が一番アクセスが多かった。

仕事はうまく進まない

仕事は何かと自分がやらないといけない作業が多い。 人が自分ごととして動いてくれていない気がしてならない。 名指しで一旦、作業を振らないとダメなんだろうなと。かなりフラストレーションが溜まっている。 そして、4月からは新しいことをしないといけないので、恐ろしさで震えている。 失敗を恐れるなとはいうものの、色々言われてしまうので、そこが辛い。

グループ内でツールを共有

効率化のツールをグループに共有してみた。 誰も反応してくれなかった。 誰かに反応してほしいから共有したわけではないので、気にしない。 ただ、他の人が情報共有してくれていたら、何かしらのリアクションはしてあげようと改めて思った。

わかったこと

モチベーションが下がってきたかも

3月の前半はかなりのモチベーションが高く、5日連続でブログを投稿できてたけど、 今週に入ってから、仕事のことを含めてモチベーションが急激に落ちている。 ブログのネタもなかなか進まないというのもあるのかな。 4月は少しペースを落とすことにする。

次にやること

OSSを探そう

OSSに貢献したいので、貢献できそうなOSSを探してみる。

4月は投稿を減らす

モチベーションが急激に落ちてしまっているので、4月は投稿を週1回ぐらいに減らす。 また、仕事も新しいことを始めるので、ペースをそちらに合わせる。

アファメーション

自己肯定感が低いので、アファメーションをやってみようかと思う。

やりたいこと

今、ぱっと思いつくやりたいことを書いておく。

  • データ分析をかじる
  • kafkaのお勉強
  • gRPC
  • クライアント周りもやらないと
  • Trelloの整理
  • Pythonの資格取ろうかな

最後に

仕事もそこそこ忙しく、ほぼ出社して通勤に疲れた。 でも、朝活/夜活はほぼやっていたし、ブログの投稿も頑張ったので、充実した3月だった。 4月は世間では新学期や入社式、入学式と新しい年度のスタート。 自分自身の環境も変化があるので、まずはその変化に慣れることを優先しておく。

ハンズオンで学ぶ:PrometheusとGrafanaの魅力

PrometheusとGrafana、Exporterを使ったことがなかったので、チュートリアルでまずは動かしてみる

Prometheusとは

Prometheusは、オープンソースのシステム監視およびアラートツールキット。メトリクスを時系列データとして収集して保存する。 Prometheusエコシステムは優秀なものがいくつかあるそうだ。

エコシステムとは、製品の連携やつながりによって成り立つ全体の大きなシステムを形成するさまを「エコシステム」と表現する。

makefri.jp

ハンズオン

わかりやすいハンズオンがQiitaにあったので、実際に触ってみることにする。 Grafanaの設定画面が変更?になっているので、一部手順が違うかも。

qiita.com

構成

ファイルの構成は以下のとおり。 docker-compose.yamlにprometheusとgrafanaのコンテナを定義して、prometheus.ymlにprometheusの設定を定義する。

.
├── docker-compose.yaml
└── prometheus
    └── prometheus.yml

用意するもの

ハンズオンに合わせて、dockerを使ってセットアップする。

version: "3"

services:
  prometheus:
    image: prom/prometheus
    container_name: prometheus
    volumes:
      - ./prometheus:/etc/prometheus/
    ports:
      - 127.0.0.1:9090:9090

  grafana:
    image: grafana/grafana
    container_name: grafana
    ports:
      - 3000:3000
global:
    scrape_interval: 15s # Set the scrape interval to every 15 seconds. Default is every 1 minute.
    evaluation_interval: 15s # Evaluate rules every 15 seconds. The default is every 1 minute.
    external_labels:
      monitor: "codelab-monitor"

rule_files:

scrape_configs:
  - job_name: "prometheus"

    static_configs:
      - targets: ["localhost:9090"]

下記のコマンドを実行し、コンテナを起動する。

docker-compose up -d

promethuesとgrafnaが起動したら、ブラウザからアクセスする。 http://localhost:9090にアクセスするとpromethuesにアクセスができ、以下の画面が表示される。

画面の[Status]メニューから[Targets]をクリックし、Endpointに対して動作していることを確認できる。

grafanaにアクセスする場合、http://localhost:3000にアクセスする。 初回起動時は、ユーザ名/パスワードに admin を入力し、ログインする。

ログインすると、以下のような画面が表示される。

prometheusと連携するには、DataSourceから設定する必要があるので、DataSourceをクリックする。 次の画面で、Prometheusをクリックする。

SettingsタグのConnectionにprometheusのURLを入力し、画面下部にある Save&testをクリックする。

コンテナで起動しているので、URLはhttp://prometheus:9090にする必要がある。

メニューからDashboards を選択し、右上のNewボタンからNew dashboardをクリックする。

Import dashboardをクリックする。

https://grafana.com/grafana/dashboards/ にアクセスするとdashboardのテンプレートがあるので、使用したいテンプレートを選択する。 使用したいテンプレートを選択し、Copy ID to clipboardをクリックする。

コピーしたIDを貼り付けて、Loadボタンをクリックし、DataSourceを選択して、Importボタンをクリックする。

インポートしたテンプレートを使ってデータが可視化されるので、あとは保存するだけ。

Exporter連携

Exporterとは

Promethuesと連携して、CPU使用率などのメトリック情報を収集するアプリケーション。

dockerファイルの変更

exporter をdockerで起動するので、docker-compose.yamlにexporterの設定を追加する。

  exporter:
    image: prom/node-exporter
    ports:
      - 9100:9100

localhost:9100を監視対象に追加。

    static_configs:
      - targets: ["localhost:9090",
                  "localhost:9100"]

DashboardをPrometheusと同じように作成

Exporterの情報を表示するテンプレートも用意されているので、それを使用してデータを可視化。

yesコマンドでCPUに負荷をかけたので、CPU使用率が上昇していることがわかる。 画面もカッコいい

最後に

簡単に動かす程度の設定はできた。実際に自分でデータを登録して、自分で作成した画面に表示することはできていないので、自分で登録したデータを表示するというところまではやってみる。 まだまだ、理解不足だなと実感。

参考

qiita.com

qiita.com

knowledge.sakura.ad.jp

zenn.dev

RabbitMQとは?チュートリアルで理解を深める

RabbitMQを使って、メッセージキューの理解を深めていく。

RabbitMQとは

オープンソースのメッセージ指向ミドルウェアで、メッセージキューを利用してアプリケーション間の非同期通信を実現するためのソフトウェア。 AMQAと呼ばれるプロトコルを使用する。

qiita.com

Kafkaとの違い

aws.amazon.com

やりたいこと

RabbitMQを介して、クライアントAからメッセージを送信して、クライアントBにメッセージを渡す。

RabbitMQのセットアップ

RabbitMQはコンテナで起動する。 RabbitMQのコンテナは、管理プラグインが有効となっているものとなっていないものでコンテナが異なる。 今回は、管理画面でキューを確認したいので、管理プラグインが有効になっているものを使用する。

https://hub.docker.com/_/rabbitmq

version: "3"

services:
  rabbitmq:
    image: rabbitmq:3-management
    environment:
      - 8080:15672
      - 5672:5672

http://localhost:8080にアクセスし、UsernameとPasswordにguestを入力し、ログインすると管理画面が表示できる。 もし、画面が表示できない場合は少し待って再度アクセスすると画面が表示できる。 少し立ち上がるのが遅いみたい。 ユーザ名やパスワードを変更したい場合は、RABBITMQ_DEFAULT_USERRABBITMQ_DEFAULT_PASSで変更できる。

クライアントの準備

www.rabbitmq.com

RabbitMQにメッセージを送信するために、Pikaをインストールする。

pip install pika

RabbitMQのHallo World

PythonでのRabbitMQとの連携のチュートリアルが公式ページにあるので、実施してみる。 まずは送信側をコードを作成し、実行してみたところメッセージも出力されたので、送信できている。

import pika

# RabbitMQに接続
connection = pika.BlockingConnection(pika.ConnectionParameters("localhost"))
channel = connection.channel()

# キューを作成
channel.queue_declare(queue="hello")

message = "Hello World"
channel.basic_publish(exchange="", routing_key="hello", body=message)
print("  [x] Send " + message)

# コネクションのクローズ
connection.close()

RabbitMQの管理画面を確認すると、Queueにメッセージ登録されていることがわかる。 (8回実行したので、グラフ上は8メッセージ溜まっている状態)

次に受信側を実行してみる。

import sys
import os
import pika


def callback(channel, method, properties, message):
    print("  [x] Received " + str(message))
    print("      method: " + str(method))
    print("      properties: " + str(properties))


def main():
    # RabbitMQに接続
    connection = pika.BlockingConnection(pika.ConnectionParameters("localhost"))
    channel = connection.channel()
    print("  [*] Waiting for message. To exit press CTRL+C")
    channel.basic_consume(queue="hello", auto_ack=True, on_message_callback=callback)
    channel.start_consuming()


if __name__ == "__main__":
    try:
        main()
    except KeyboardInterrupt:
        print("Interrupted")
        try:
            sys.exit(0)
        except SystemExit:
            os._exit(0)

蓄積していたメッセージを全て受信し、出力できた。 RabbitMQの管理画面からもQueueにあったメッセージが全て配信されていることがわかる。

  [*] Waiting for message. To exit press CTRL+C
  [x] Received b'Hello World'
      method: <Basic.Deliver(['consumer_tag=ctag1.be2617d6173e46de8dd43a444597d439', 'delivery_tag=1', 'exchange=', 'redelivered=True', 'routing_key=hello'])>
      properties: <BasicProperties>
  [x] Received b'Hello World'
      method: <Basic.Deliver(['consumer_tag=ctag1.be2617d6173e46de8dd43a444597d439', 'delivery_tag=2', 'exchange=', 'redelivered=False', 'routing_key=hello'])>
      properties: <BasicProperties>
  [x] Received b'Hello World'
      method: <Basic.Deliver(['consumer_tag=ctag1.be2617d6173e46de8dd43a444597d439', 'delivery_tag=3', 'exchange=', 'redelivered=False', 'routing_key=hello'])>
      properties: <BasicProperties>
  [x] Received b'Hello World'
      method: <Basic.Deliver(['consumer_tag=ctag1.be2617d6173e46de8dd43a444597d439', 'delivery_tag=4', 'exchange=', 'redelivered=False', 'routing_key=hello'])>
      properties: <BasicProperties>
  [x] Received b'Hello World'
      method: <Basic.Deliver(['consumer_tag=ctag1.be2617d6173e46de8dd43a444597d439', 'delivery_tag=5', 'exchange=', 'redelivered=False', 'routing_key=hello'])>
      properties: <BasicProperties>
  [x] Received b'Hello World'
      method: <Basic.Deliver(['consumer_tag=ctag1.be2617d6173e46de8dd43a444597d439', 'delivery_tag=6', 'exchange=', 'redelivered=False', 'routing_key=hello'])>
      properties: <BasicProperties>
  [x] Received b'Hello World'
      method: <Basic.Deliver(['consumer_tag=ctag1.be2617d6173e46de8dd43a444597d439', 'delivery_tag=7', 'exchange=', 'redelivered=False', 'routing_key=hello'])>
      properties: <BasicProperties>
  [x] Received b'Hello World'
      method: <Basic.Deliver(['consumer_tag=ctag1.be2617d6173e46de8dd43a444597d439', 'delivery_tag=8', 'exchange=', 'redelivered=False', 'routing_key=hello'])>
      properties: <BasicProperties>

最後に

RabbitMQのチュートリアルのHelloWorldをやってみた。 管理画面でキューにメッセージが蓄積されていることも確認できたし、どのように実装するのかもなんとなくイメージが掴めた。 次回は、チュートリアルの続きを進めて、RabbitMQの使い方の理解を深める。

Streamlitを使用した2軸の複合グラフ作成手順

PB曲線をStreamlitで作成したいので、2軸グラフにチャレンジ。

Streamlitで2軸の複合グラフを作成するためには、altair_chartを使用する。 altair_chartは、Altairライブラリを使用して、グラフを表示するAPI。 そのため、altair_chartを使用する場合は、Altairライブラリのマニュアルを見て作成が必要になる。

docs.streamlit.io

github.com

作ってみる

作ってみるのは、試験の残項目数とバグ件数を2軸の複合グラフで表示する。 下記のサイトを参考にした。

altair-viz.github.io

大きなポイントとしては、2つ。 - それぞれのグラフのオブジェクトを作成する - 2つのグラフを階層化して1つのグラフにする

それぞれのグラフのオブジェクトを作成する

mark_line関数とencode関数をコールしてそれぞれのグラフのオブジェクトを作成する。

altair-viz.github.io

remaning_test_items = base.mark_line(color="red").encode(
    alt.Y("残項目数", axis=alt.Axis(title="残項目数"))
)

bugs = base.mark_line().encode(alt.Y("バグ数", axis=alt.Axis(title="バグ数")))

2つのグラフを階層化して1つのグラフにする

複数のグラフを1つにする場合、layerresolve_scaleを使用する。 まずは、2つのグラフを階層化(1つにする)するために、alt.layerを実行する。 その際に、パラメータは1つにしたいグラフのオブジェクトを指定する。 次に、複合グラフを作成するための、resolve_scale を実行する。 Y軸は別々にしたいので、y="independentをパラメータとして指定する。

altair-viz.github.io

pb_chart = alt.layer(remaning_test_items, bugs).resolve_scale(y="independent")

日付通りに表示されない

X軸は、dateを指定しているが、これだと日付と認識してくれない。 日付である場合は、対象のカラム名の後に、:Tをつける必要がある。

altair-viz.github.io

base = alt.Chart(chart_data).encode(x="date")
base = alt.Chart(chart_data).encode(x="date:T")

表示できた

表示できた。X軸の表記などの細かいところはあるが、期待する表示はできた。

最後に

Altairを使ってグラフを表示できた。 2軸表示するために、layerresolve_scaleが必要であることに気づくのが遅かった。 サンプルを少しずつ変更して表示確認すればよかったと少し後悔とちゃんとソースの読んでいる関数を調べないといけないなと 改めて感じた。

参考

kotapontan.hatenablog.com

import streamlit as st
import pandas as pd
import altair as alt

st.title("PB曲線グラフ")


chart_data = pd.read_csv("./data.csv")

base = alt.Chart(chart_data).encode(x="date:T")
remaning_test_items = base.mark_line(color="red").encode(
    alt.Y("残項目数", axis=alt.Axis(title="残項目数"))
)

bugs = base.mark_line().encode(alt.Y("バグ数", axis=alt.Axis(title="バグ数")))

pb_chart = alt.layer(remaning_test_items, bugs).resolve_scale(y="independent")
st.altair_chart(pb_chart, use_container_width=True)
  • データ
date,残項目数,バグ数,,
2024-03-01,200,0,,
2024-03-02,190,0,,
2024-03-03,184,1,,
2024-03-04,182,1,,
2024-03-05,173,1,,
2024-03-06,168,2,,
2024-03-07,148,2,,
2024-03-08,132,2,,
2024-03-09,120,2,,
2024-03-10,117,3,,
2024-03-11,102,3,,
2024-03-12,91,3,,
2024-03-13,83,3,,
2024-03-14,76,3,,
2024-03-15,53,3,,
2024-03-16,43,3,,
2024-03-17,37,3,,
2024-03-18,23,3,,
2024-03-19,21,3,,
2024-03-20,19,3,,
2024-03-21,7,3,,
2024-03-22,3,3,,
2024-03-23,1,3,,
2024-03-24,0,3,,

Streamlitで簡単なフォームを作成にチャレンジ

前回はstreamlitを使ってみたので、今回はstreamlitで簡単なフォーム画面を作成してみる。

kotapontan.hatenablog.com

作成する画面

簡単な映画の評価アンケートを入力する画面を作成する。
性別と映画の視聴頻度は、ラジオボタンを使って、評価はスライダーを使用する。
年齢は数字用のテキストフィールドを使用。
また、送信用のボタンをクリックした際に、データベースにデータを登録する。

データベースは、timescaledbを使用することにする。 timescaledbにテーブルと1件のデータを登録しておく。

CREATE TABLE movie_questionnaire (
    time timestamp NOT NULL,
    gender TEXT NOT NULL,
    age int NOT NULL,
    frequency_show TEXT NOT NULL,
    good_movie int NOT NULL
);

SELECT create_hypertable('movie_questionnaire', 'time');
INSERT INTO movie_questionnaire VALUES ('2024-02-27 06:47:00', 'Male', 25, '見ない', 5);

streamlitで画面を作成。

import streamlit as st

from sqlalchemy import create_engine
from sqlalchemy.types import Integer, String, DateTime
from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy.orm import sessionmaker
import datetime

class Base(DeclarativeBase):
    pass

class Moviequestionnaire(Base):
    __tablename__ = "movie_questionnaire"

    time: Mapped[datetime.datetime] = mapped_column(DateTime, primary_key=True, nullable=False)
    gender: Mapped[str] = mapped_column(String, nullable=False)
    age: Mapped[int] = mapped_column(Integer, nullable=False)
    frequency_show: Mapped[str] = mapped_column(String, nullable=False)
    good_movie: Mapped[int] = mapped_column(Integer, nullable=False)

# DBエンジンを作成
url = "postgresql://postgres:example@localhost:5432/postgres"
engine = create_engine(url, echo=True)
# セッションの作成
SessionClass = sessionmaker(engine)
session = SessionClass()


with st.form("my_form"):
    st.title("Let's try!!!")
    gender = st.radio("Gender", ["Male", "Female"])
    age = st.number_input("Age", min_value=20, max_value=60, format="%d")
    frequency_show = st.radio('映画はよく見ますか?', ["月5回以上", "月1回", "半年に1回", "見ない"])
    good_movie = st.slider("今回の映画の評価を教えてください(0-5)", 0, 5, 0)

    submitted = st.form_submit_button("送信")
    if submitted:
        movie_questionnaire = Moviequestionnaire(time=datetime.datetime.now(),
                                                 gender=gender,
                                                 age=age,
                                                 frequency_show=frequency_show,
                                                 good_movie=good_movie)
        session.add(movie_questionnaire)
        session.commit()

streamlitコマンドで画面を起動すると、画面が表示される。

streamlit run main.py

登録前のデータを確認

select * from movie_questionnaire;
            time            | gender | age | frequency_show | good_movie 
----------------------------+--------+-----+----------------+------------
 2024-02-27 06:47:00        | Male   |  25 | 見ない         |          5
 2024-02-28 06:34:58.851004 | Male   |  20 | 月5回以上      |          1
 2024-02-28 06:53:32.887118 | Female |  40 | 月1回          |          4
(3 rows)

登録するデータを画面で設定して、送信ボタンをクリックする。
データベースを検索して、設定したデータが登録されていることを確認。

select * from movie_questionnaire;
            time            | gender | age | frequency_show | good_movie 
----------------------------+--------+-----+----------------+------------
 2024-02-27 06:47:00        | Male   |  25 | 見ない         |          5
 2024-02-28 06:34:58.851004 | Male   |  20 | 月5回以上      |          1
 2024-02-28 06:53:32.887118 | Female |  40 | 月1回          |          4
 2024-02-29 21:53:45.414323 | Male   |  32 | 月1回          |          3
(4 rows)

まとめ

streamlitを使ったフォーム画面を作成してみた。 本当に数行でフォーム画面が簡単に作れて、データベースにもデータを登録することができた。 本当はグラフまでやってみたかったが、timescaledbの知識不足とPandasの知識不足でやり切れなかった。
ちょっとお勉強して、グラフにもチャレンジしてみる。