ヒトリ歩き

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

ハンズオンで学ぶ: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の知識不足でやり切れなかった。
ちょっとお勉強して、グラフにもチャレンジしてみる。

データ分析の第一歩:Pandasを使ったデータ型と操作

streamlitでも出てきたpandasを使ったことがないので、基本的な部分をやってみる。
これを使いこなして、品質データもサクッと集計したり消化スケジュールもサクッとグラフ化して雑務の工数を減らしたい。

pandasのデータ型

Pandasのデータ型としてはSeriesとDataFrameという2つのものがある。

Series

1次元データ構造と言われる1列のみのデータ型。

s = [1, 2, 3, 4, 5]
series = pd.Series(s)
print(series)

左側の番号が行ラベルと言われるindex indexを指定しない場合、0から開始になる。

0    1
1    2
2    3
3    4
4    5
dtype: int64

indexを指定する場合、indexパラメータに値を設定。

s = [1, 2, 3, 4, 5]
index = ["a", "b", "c", "d", "e"]
series = pd.Series(s, index=index)
print(series)

・出力結果
a    1
b    2
c    3
d    4
e    5

DataFrame

2次元データ構造と言われるデータ構造。 2次元配列で表すデータよりは、Excelで作る表をイメージするとわかりやすい。

data = [
    ["tanaka", 20, "female", 1, 600],
    ["suzuki", 30, "male", 8, 492],
    ["tanaka", 23, "male", 3, 890],
    ["sato", 28, "male", 4, 833],
]
column = ["name", "age", "m/f", "y of experience", "score"]
df = pd.DataFrame(data=data, columns=column)
print(df)

出力結果はこうなる。

        0   1       2  3    4
0  tanaka  20  female  1  600
1  suzuki  30    male  8  492
2  tanaka  23    male  3  890
3    sato  28    male  4  833

DataFrameは、indexとcolumnsがある。 indexを指定できたように、columnsも列名を指定できる。

data = [
    ["tanaka", 20, "female", 1, 600],
    ["suzuki", 30, "male", 8, 492],
    ["tanaka", 23, "male", 3, 890],
    ["sato", 28, "male", 4, 833],
]
column = ["name", "age", "m/f", "y of experience", "score"]
df = pd.DataFrame(data=data, columns=column)
print(df)

・出力結果
     name  age     m/f  y of experience  score
0  tanaka   20  female                1    600
1  suzuki   30    male                8    492
2  tanaka   23    male                3    890
3    sato   28    male                4    833

データの取得

データの取得方法にはいくつか方法がある。

特定の列または行を取得

pandasは、[]にキーを入力すると列名と判断し、スライスを入力すると行ラベルと判断して処理する。

www.sejuku.net

import pandas as pd

# DataFrame
# 2次元データ構造
data = [
    ["tanaka", 20, "female", 1, 600],
    ["suzuki", 30, "male", 8, 492],
    ["tanaka", 23, "male", 3, 890],
    ["sato", 28, "male", 4, 833],
]
column = ["name", "age", "m/f", "y of experience", "score"]
df = pd.DataFrame(data=data, columns=column)
print(df)

# 特定の列を取得
print("ageカラムを取得")
print(df["age"])

# 特定の行を取得
print("3行目を取得")
print(df[2:3])

# 実行結果
ageカラムを取得
0    20
1    30
2    23
3    28
Name: age, dtype: int64
3行目を取得
     name  age   m/f  y of experience  score
2  tanaka   23  male                3    890

特定の列と行の取得

locを使用した列と行の取得は列名と行名を使用できる。

import pandas as pd

# DataFrame
# 2次元データ構造
data = [
    ["tanaka", 20, "female", 1, 600],
    ["suzuki", 30, "male", 8, 492],
    ["tanaka", 23, "male", 3, 890],
    ["sato", 28, "male", 4, 833],
    ["takano", None, "male", 20, 234],
]
column = ["name", "age", "m/f", "y of experience", "score"]
index = ["0001", "0002", "0003", "0004", "0005"]
df = pd.DataFrame(data=data, columns=column, index=index)
print(df)

# 特定の列と行を取得
print("loc関数で取得")
print(df.loc["0003", "age":"m/f"])
print(df.loc["0004":])
# 実行結果
loc関数で取得
age    23.0
m/f    male
Name: 0003, dtype: object
        name   age   m/f  y of experience  score
0004    sato  28.0  male                4    833
0005  takano   NaN  male               20    234

ilocの場合は、列名や行名は使用できない。 インデックス番号を使用する必要がある。

import pandas as pd

# DataFrame
# 2次元データ構造
data = [
    ["tanaka", 20, "female", 1, 600],
    ["suzuki", 30, "male", 8, 492],
    ["tanaka", 23, "male", 3, 890],
    ["sato", 28, "male", 4, 833],
    ["takano", None, "male", 20, 234],
]
column = ["name", "age", "m/f", "y of experience", "score"]
index = ["0001", "0002", "0003", "0004", "0005"]
df = pd.DataFrame(data=data, columns=column, index=index)
print(df)

# 特定の列と行を取得
print("ilocを使用")
print(df.iloc[2:, 1:3])
print(df.iloc[3:])
ilocを使用
       age   m/f
0003  23.0  male
0004  28.0  male
0005   NaN  male
        name   age   m/f  y of experience  score
0004    sato  28.0  male                4    833
0005  takano   NaN  male               20    234

locとilocで同じような使い方だが、行列のスライスの指定の結果が異なる。 locの場合は、スライスの最後のインデックスが含まれるが、ilocの場合は、スライスの最後のインデックスは含まれない。

先頭と末尾の取得

先頭のデータを取得する際は、head関数を使用する。 また、末尾のデータを取得する場合は、tail関数を使用する。 ともにデフォルトは5件取得。 ここでは、2を指定することで、2件取得している。

import pandas as pd
# DataFrame
# 2次元データ構造
data = [
    ["tanaka", 20, "female", 1, 600],
    ["suzuki", 30, "male", 8, 492],
    ["tanaka", 23, "male", 3, 890],
    ["sato", 28, "male", 4, 833],
    ["takano", None, "male", 20, 234],
]
column = ["name", "age", "m/f", "y of experience", "score"]
index = ["0001", "0002", "0003", "0004", "0005"]
df = pd.DataFrame(data=data, columns=column, index=index)

# 先頭と末尾を取得
print("= 先頭 ==========")
print(df.head(2))

print("= 末尾 ==========")
print(df.tail(2))
# 実行結果
= 先頭 ==========
        name   age     m/f  y of experience  score
0001  tanaka  20.0  female                1    600
0002  suzuki  30.0    male                8    492
= 末尾 ==========
        name   age   m/f  y of experience  score
0004    sato  28.0  male                4    833
0005  takano   NaN  male               20    234

条件指定での行取得

query関数を使用することで、データベースを検索するように条件を指定してデータを取得できる。

import pandas as pd

# DataFrame
# 2次元データ構造
data = [
    ["tanaka", 20, "female", 1, 600],
    ["suzuki", 30, "male", 8, 492],
    ["tanaka", 23, "male", 3, 890],
    ["sato", 28, "male", 4, 833],
    ["takano", None, "male", 20, 234],
]
column = ["name", "age", "m/f", "y of experience", "score"]
index = ["0001", "0002", "0003", "0004", "0005"]
df = pd.DataFrame(data=data, columns=column, index=index)

# 条件指定
print("== ageが30より小さい行を取得 ======")
print(df.query("age < 30"))
print("== nameがtanakaの行を取得 ======")
print(df.query("name == 'tanaka'"))
print("== nameがtanakaかつageが23の行を取得 ======")
print(df.query("name == 'tanaka' and age == 23"))
# 実行結果
== ageが30より小さい行を取得 ======
        name   age     m/f  y of experience  score
0001  tanaka  20.0  female                1    600
0003  tanaka  23.0    male                3    890
0004    sato  28.0    male                4    833
== nameがtanakaの行を取得 ======
        name   age     m/f  y of experience  score
0001  tanaka  20.0  female                1    600
0003  tanaka  23.0    male                3    890
== nameがtanakaかつageが23の行を取得 ======
        name   age   m/f  y of experience  score
0003  tanaka  23.0  male                3    890

列と行の追加

列を追加する場合、[列名]=値またはassign関数を使用する。 assign関数を使用した場合、新しいDataFrameのオブジェクトを返す。 行を追加する場合、loc[行名] = 値で追加できる。 また、concat関数でDataFrame同士を連結する方法もある。

import pandas as pd

data = [["tanaka", 20, "female", 1, 600], ["suzuki", 30, "male", 8, 492]]
column = ["name", "age", "m/f", "y of experience", "score"]
df = pd.DataFrame(data=data, columns=column)
print("== 追加前 ================")
print(df)
print("== 列を追加1 ==============")
df["like"] = ["movie", "cook"]
print(df)
print("== 列を追加2 ==============")
df = df.assign(food=["sushi", "apple"])
print(df)
print("== 行を追加1 ==============")
df.loc[2] = ["yamada", 40, "male", 20, 500, "cycle", "beaf"]
print(df)

最後に

pandasの基本的な操作をやってみた。 集計とかその辺をもう少しやってみて、品質データの分析を簡単に集計できるようになったら、嬉しいな。

参考

qiita.com

qiita.com

qiita.com

リーダーの課題解決アプローチ。カネヴィンフレームワーク

ウクライナ軍に入隊したアジャイルコーチがカネヴィンフレームワークを使って、課題等を解決したとのことで、カネヴィンフレームワークというのを初めて気になり、調べてみた。

www.publickey1.jp

トラブルが発生すると、日本企業では原因を探し始める。
欠点や不具合、未達成な成果を探して、原因を分析、真の原因を見つけて、対策を立てる。
これは、不具合原因追及型の問題解決ギャップアプローチと呼ばれる問題解決手法。

この解決手法が全ての問題で解決されるわけではなく、組織のリーダーは課題や問題に囲まれているので、問題や課題によって解決方法が異なってくる。

では、どうやって解決方法を見つけていけばいいのか? そのためのフレームワークがカネヴィンフレーム。

カネヴィンフレームワークとは

ディビット・スノードン教授によって、問題を分類し、その問題に対するアプローチを定義したフレームワーク

問題は単純系複雑系困難系カオス系の4つのカテゴリに分け、カテゴリごとにアクションが異なる。

解決方法は見えている

単純系と呼ばれるものは、問題の原因が誰にでもわかる問題のこと。 ソフトウェアの問題で言うと、エラーログを見ればすぐに修正箇所がわかるだと考えられる。

問題解決に専門的な知識が必要

困難系と呼ばれるものは、因果関係が複雑であり、原因が分からない問題のこと。
ただし、分析すれば原因が見えるもの。
ソフトウェアの品質分析がこれにあたるのではないか考えます。いくつかのバグが発生しており、そのバグが発生した原因を分析し、対策を実施する。 PDCAサイクルの、CheckとActionもここに入るのかな。

Try And Error。やるっきゃない

複雑系の問題は因果関係が明確になっていない、かつ、原因が分からない問題のこと。 組織で起こりがちな問題が多く、未来のありたい姿に向けて未来創造型課題達成型を利用して解決していく。 自分の身近なところだと、プロジェクトで効率化をしているのに、メンバの稼働が下がらないとか。

先が見えない。恐ろしい。

カオス系は、因果関係も見えず、突発的に発生した問題をすぐに対処しなければならない問題のこと。 このカオス系は、必要なありとあらゆるアクションを実行し、一刻もはやい対応が求められる。 コロナウィルスの感染拡大がこれにあたる。 自分の身近なところだと、お客さまの利益に関わるような障害が発生した場合にあたる。 考えただけでも、ぞっとします。

最後に

カネヴィンフレームワークというものを知って、解決に向けてどのようなアプローチを取ればいいのかが考えやすくなると思う。 早速、このフレームワークを取り入れて、業務に活かしたい。

参考

successpoint.co.jp

heart-quake.com

successpoint.co.jp

ソート可能な一意な値を生成するULIDが超絶便利

UUIDは一意な値だが、ソートできない問題がある。
その課題を解決したソート可能な一意な値を生成できるULIDは非常に便利!!

ULIDとは

ULIDは、ソート可能な一意の識別子。
辞書順にソート可能であり、UUIDと128bitの互換性がある。
ULIDの構造は、48bitがタイムスタンプとなっており、80bitがランダムな値。

インストール方法

python-ulidをインストールするだけ。

pip install python-ulid

使ってみた

基本的な使い方は下記のページに載っている。

github.com

  • 現在のタイムスタンプからULIDオブジェクトを生成

現在のタイムスタンプからULIDオブジェクトを生成する場合は、ULIDを実行する。

# 現在のタイムスタンプからULIDオブジェクトを生成
ulid = ULID()
print("ULID = " + str(ulid))
  • タイムスタンプ指定でULIDオブジェクトを生成

指定のタイムスタンプからULIDオブジェクトを生成する場合、ULIDクラスのfrom_datetimeクラスメソッドを実行する。

# 2023-03-02 18:00:00 からULIDオブジェクトを生成
specfic_datetime = datetime.datetime.strptime("2023-03-02 18:00:00 +0000", "%Y-%m-%d %H:%M:%S %z")
ulid = ULID.from_datetime(specfic_datetime)
print("ULID = " + str(ulid))
  • ULIDに含まれる日時を確認

ULIDに含まれる日時を確認する場合、datetimeプロパティを確認する。

# ulidに含まれる日時を確認
print("Datetime = " + ulid.datetime.strftime('%Y-%m-%d %H:%M:%S'))
  • ULIDからUUIDを生成

ULIDからUUIDを生成する場合、to_uuidメソッドを実行する。

# UUIDに変換
uuid_from_ulid = ulid.to_uuid()
print("UUID = " + str(uuid_from_ulid))
  • UUIDからULIDを生成

UUIDからULIDオブジェクトを生成する場合、ULIDクラスのfrom_uuidクラスメソッドを実行する。

# UUIDからulidを生成
ULID.from_uuid(uuid_from_ulid)
print("ULID from uuid = " + str(ulid2))

ソートしてみる

日時指定でULIDオブジェクトを生成する。このときに1日単位でずらして生成。 順序をばらばらに挿入したリストを生成して、sorted関数でソートした結果を確認する。

def sort_execute():
    
    # 日時指定で1日ずらして、ULIDオブジェクトを生成
    specfic_datetime = datetime.datetime.strptime("2023-03-02 18:00:00 +0000", "%Y-%m-%d %H:%M:%S %z")
    ulid1 = ULID.from_datetime(specfic_datetime).to_uuid()
    print("ulid1 = " + str(ulid1))

    specfic_datetime = datetime.datetime.strptime("2023-03-03 18:00:00 +0000", "%Y-%m-%d %H:%M:%S %z")
    ulid2 = ULID.from_datetime(specfic_datetime).to_uuid()
    print("ulid2 = " + str(ulid2))

    specfic_datetime = datetime.datetime.strptime("2023-03-04 18:00:00 +0000", "%Y-%m-%d %H:%M:%S %z")
    ulid3 = ULID.from_datetime(specfic_datetime).to_uuid()
    print("ulid3 = " + str(ulid3))

    specfic_datetime = datetime.datetime.strptime("2023-03-05 18:00:00 +0000", "%Y-%m-%d %H:%M:%S %z")
    ulid4 = ULID.from_datetime(specfic_datetime).to_uuid()
    print("ulid4 = " + str(ulid4))

    # 整列した順番のリストを作成
    expected_list = list()
    expected_list.append(ulid1)
    expected_list.append(ulid2)
    expected_list.append(ulid3)
    expected_list.append(ulid4)

    # 順番をばらばらにしたリストを作成
    list_ulid = list()
    list_ulid.append(ulid2)
    list_ulid.append(ulid4)
    list_ulid.append(ulid1)
    list_ulid.append(ulid3)

    # 整列した順番のリストとばらばらなリストを比較
    print("Before = " + str(list_ulid))
    print("Is Same = " + str(expected_list == list_ulid))

    # ソート後にソートしたリストと整列した順番のリストを比較
    sorted_list = sorted(list_ulid)
    print("After  = " + str(sorted_list))
    print("Is Same = " + str(expected_list == sorted_list))

実行結果を確認すると、ソートできていることがわかる。

ulid1 = 0186a37b-1d00-223a-1718-56350b798aec
ulid2 = 0186a8a1-7900-b053-3768-799da20ec1a3
ulid3 = 0186adc7-d500-9872-0ecd-d7341e5b680a
ulid4 = 0186b2ee-3100-7041-5e91-7f7813f8d84d
Before = [UUID('0186a8a1-7900-b053-3768-799da20ec1a3'), UUID('0186b2ee-3100-7041-5e91-7f7813f8d84d'), UUID('0186a37b-1d00-223a-1718-56350b798aec'), UUID('0186adc7-d500-9872-0ecd-d7341e5b680a')]
Is Same = False
After  = [UUID('0186a37b-1d00-223a-1718-56350b798aec'), UUID('0186a8a1-7900-b053-3768-799da20ec1a3'), UUID('0186adc7-d500-9872-0ecd-d7341e5b680a'), UUID('0186b2ee-3100-7041-5e91-7f7813f8d84d')]
Is Same = True

uuidモジュールで生成したuuidをULIDオブジェクトに変換できる?

from_uuidクラスメソッドを使用すれば変換できる。 ただし、日時は異常な日時になる。

ulid2 = ULID.from_uuid(uuid.uuid4())
print("ULID from uuid = " + str(ulid2))
print(ulid2.datetime)
ULID from uuid = 1S57355C1B940S5RB1VZD2P2FW
3961-09-08 21:15:50.186996+00:00

PostgreSQLのorder by でソート

ULIDで生成したUUIDをPostgreSQLのテーブルに挿入し、OrderByでソートしても期待する順になるか確認してみた。 先ほどのリストのときと同じように、日時をずらしたULIDオブジェクトを生成、ULIDから生成したUUIDをばらばらに挿入。

from ulid import ULID
import datetime
import uuid
from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
from sqlalchemy import select
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
import uuid

class Base(DeclarativeBase):
    pass

class UUID_Sample(Base):
    __tablename__ = 'uuid_sample'

    id: Mapped[uuid.UUID] = mapped_column(primary_key=True)

# DBエンジンを作成
url = "postgresql://postgres:example@localhost:5432/postgres"
engine = create_engine(url, echo=False)

# テーブルをDBに作成
Base.metadata.create_all(engine)
SessionClass = sessionmaker(engine)
session = SessionClass()

def use_db():
    
    # 日時指定で1日ずらして、ULIDオブジェクトを生成
    specfic_datetime = datetime.datetime.strptime("2023-03-02 18:00:00 +0000", "%Y-%m-%d %H:%M:%S %z")
    ulid1 = ULID.from_datetime(specfic_datetime).to_uuid()
    print("ulid1 = " + str(ulid1))
    ulid1_obj = UUID_Sample(id=ulid1)

    specfic_datetime = datetime.datetime.strptime("2023-03-03 18:00:00 +0000", "%Y-%m-%d %H:%M:%S %z")
    ulid2 = ULID.from_datetime(specfic_datetime).to_uuid()
    print("ulid2 = " + str(ulid2))
    ulid2_obj = UUID_Sample(id=ulid2)

    specfic_datetime = datetime.datetime.strptime("2023-03-04 18:00:00 +0000", "%Y-%m-%d %H:%M:%S %z")
    ulid3 = ULID.from_datetime(specfic_datetime).to_uuid()
    print("ulid3 = " + str(ulid3))
    ulid3_obj = UUID_Sample(id=ulid3)

    specfic_datetime = datetime.datetime.strptime("2023-03-05 18:00:00 +0000", "%Y-%m-%d %H:%M:%S %z")
    ulid4 = ULID.from_datetime(specfic_datetime).to_uuid()
    print("ulid4 = " + str(ulid4))
    ulid4_obj = UUID_Sample(id=ulid4)
    session.add_all([ulid2_obj, ulid4_obj, ulid1_obj, ulid3_obj])
    session.commit()

    # 整列した順番のリストを作成
    expected_list = list()
    expected_list.append(ulid1)
    expected_list.append(ulid2)
    expected_list.append(ulid3)
    expected_list.append(ulid4)

    stmt = select(UUID_Sample)
    results = session.scalars(stmt)
    print("Not use order by")
    i = 0
    for result in results:
        print("id = " + str(result.id))
        print("Is Sample = " + str(result.id == expected_list[i]))
        i += 1

    print("Use order by")
    stmt = select(UUID_Sample).order_by(UUID_Sample.id)
    results = session.scalars(stmt)
    i = 0
    for result in results:
        print("id = " + str(result.id))
        print("Is Sample = " + str(result.id == expected_list[i]))
        i += 1

結果はOrder byでソートされていることを確認できた。

ulid1 = 0186a37b-1d00-20ba-f545-49810830ce9d
ulid2 = 0186a8a1-7900-d435-b373-beeca474a2ed
ulid3 = 0186adc7-d500-26fa-2d95-ee12269bf390
ulid4 = 0186b2ee-3100-9cce-b5d3-4ab2c0ca769d
Not use order by
id = 0186a8a1-7900-d435-b373-beeca474a2ed
id = 0186b2ee-3100-9cce-b5d3-4ab2c0ca769d
id = 0186a37b-1d00-20ba-f545-49810830ce9d
id = 0186adc7-d500-26fa-2d95-ee12269bf390
Use order by
id = 0186a37b-1d00-20ba-f545-49810830ce9d
id = 0186a8a1-7900-d435-b373-beeca474a2ed
id = 0186adc7-d500-26fa-2d95-ee12269bf390
id = 0186b2ee-3100-9cce-b5d3-4ab2c0ca769d

最後に

UUIDは一意な値であるが、ソートができない課題を持っている。
ULIDがその課題を解決してくれている。
ULIDで生成したUUIDであればソート可能な値となっているので、テーブルのINDEXはUUIDにだけ設定しておけば不要なINDEXが増えなくてすむのかなと考える。
非常に便利なので、積極的に使っていきたい。

参考

ULIDに関するライブラリが多いとのこと。 今回使用したpython-ulidはメンテされている。

yu00sasaki.com

qiita.com

同一ミリ秒内での生成でも、同じミリ秒内に280 を超えない限り、ソート順が保証されるみたいで、良さそうです。

ここ重要ですね。同一ミリ秒で生成されることはないだろうかと。