AgenticWorkflow構築のためのライブラリ比較 〜LangChain・GoogleADK・PydanticAIを使ってみた〜

こんにちは、テックドクターCTOの佐藤です。

最近、生成AIや大規模言語モデル(LLM)の発展により、複雑なタスクを自律的にこなすAIが注目されています。今回紹介するAgenticWorkflowは、こうしたAIの力を最大限に引き出すためのしくみです。

このエントリではAgenticWorkflow自体の紹介にくわえ、その実装のためのライブラリ3つを比較した結果をご紹介します。

AgenticWorkflowとは何か、なぜ注目されているのか

AgenticWorkflowとは、AIやプログラムが「自分で考えて動く」ようにする仕組み、あるいはそのための考え方のことです。AIがある目的を達成するために、都度人間の指示を受けるのではなく、自分で手順を考えたり、必要な作業を順番に進めたりできるようにします。

これは単なる自動化とは異なります。これまで行われていた自動化では、あらかじめ決められた流れに沿ってAIやプログラムが動くことが多かったと思います。一方AgenticWorkflowではAIが自分で状況に応じて柔軟に判断し、最適な行動を選択するという違いがあります。

概念説明図
weaviate.io より引用、日本語訳)

最近では業務の自動化や効率化、新しいサービスの開発など、さまざまな場面でAgenticWorkflowの活用が広がっています。それにともなってツールやSDKもさまざまなものが登場しています。

比較対象SDKの紹介

弊社ではバックエンドをPython + FastAPIで構築していることが多いので、その構成に組み込み可能な3つのライブラリ、LangChain、 GoogleADK、 PydanticAIを比較しました。

まずはそれぞれの概要を紹介します。

LangChain

大規模言語モデル(LLM)を活用したアプリケーション開発のためのオープンソースフレームワークです。LLMアプリの開発から運用までの工程を簡素化してくれます。各種コンポーネントが豊富で、外部サービスとの連携機能も充実しています。

対話エージェントや質問応答システム、RAG(Retrieval-Augmented Generation)などの構築に幅広く使われていて、LLM・ベクトルDB・ツール類を組み合わせたアプリを素早く開発できます。

GoogleADK

複雑なタスクやワークフローをこなす対話型/非対話型のエージェントを構築・管理・評価・デプロイするためのフレームワークです。主にGoogle CloudのVertex AI向けに提供されていて、GeminiなどのLLMを活用したマルチエージェントシステムや、企業向け対話ボットの開発に使用されます。

開発からデプロイまでを統合的にサポートするため、大規模な対話エージェントをスケーラブルに構築できます。

PydanticAI

Python製のエージェントフレームワークです。バリデーションライブラリとしておなじみのPydanticのチームによって開発されました。ジェネレーティブAIアプリを型安全かつ効率的に構築し、プロダクション品質に耐えるものにすることを目指しているようです。

OpenAIやAnthropic、Geminiなど複数のLLMに対応していて、Pydanticによる出力検証・構造化により一貫性のある応答を得られます。FastAPIがウェブ開発を革新したように、LLMを活用したアプリ開発を開発者フレンドリーにすることがコンセプトだそうです。

サンプルコード

比較に使用したコードはGitHubに置いてあります。

github.com

実行後、最初にライブラリを選択したのち、コマンドラインで都市名をインプットすると

  • APIで都市名を緯度経度に変換
  • APIで緯度経度から天気を取得
  • 天気を自然文にして、コマンドラインに出力

という流れで、AIがツールを選択して動作します。LLMはGeminiを利用します。

基本的には3つのライブラリ全て同じ流れです。APIを呼び出すためのPythonの関数をツールとして渡して、そのツールをLLMの判断の元に利用してレスポンスを返します。

※ちなみにこのサンプルコードは、Github IssueとPRを紐付けて、Claude Codeでそれらを操作して書いてもらいました。

使用感

実際使ってみての使用感の違いです。

LangChain

新規のプロダクトなどに手軽にAgenticWorkflowを導入したい場合、LangChainは向いていると思いました。多彩な外部連携やLangGraphによる状態管理、LangSmithによる入出力ログの管理など、必要な機能を手軽に試すことができます。

実は2024年頃まではバージョンアップごとに互換性を壊す変更が多く、LangChainを本番で安定運用するのは難しいと感じていました。ただ最近は比較的安定してきていると感じます。

注意点としてはメモリ以外の永続セッション管理機能が限定的なので、必要に応じて開発者が外部ストレージ等に履歴を保存・復元する機能を実装する必要があります。

GoogleADK

ADKは既にGCPやVertextAIを使っている人に最適だと思います。外部API統合として、GCPのリソースやGoogle検索を使うことができたり、マルチエージェントやエージェント同士の連携、非同期実行やセッション管理など様々な機能が本番運用を考えて用意されています。

ただ入力や出力のロギングは、現時点ではCloudLoggingに吐き出した後BigQueryに格納するなど自分で行う必要があり、その点に関しては他のツールの方がお手軽にできると思いました。

PydanticAI

既にPydanticをプロダクトに組み込んでいる場合はPydanticAIが最適です。LLMの入出力をPydanticのモデルで書いたり、モデルのdescriptionフィールドを参照して入出力値の説明をそのまま入れたりと、手軽に導入することができます。

またLogfireと連携して、比較的簡単にロギングをする事ができます。ただし、LangChainと同様にメモリ以外の永続セッション管理機能は限定的であり、必要に応じて開発者が外部ストレージ等を用いて履歴を保存・復元する実装する必要があります。

今後の検討

今回は簡単なサンプルのみをご紹介しましたが、今後は実際にLLMを組み込んだ製品を作るにあたり

  • 会話履歴・メモリ
  • LangSmithやLogfireを使った入出力ロギング
  • ステートグラフやサブタスクなどの導入

などについても書けたらと思います。

参考になれば幸いです。

似顔絵
書いた人:佐藤

月経周期による女性の体調不良ってどんなもの? 〜日々のアンケートからわかったこと〜

こんにちは、データサイエンスチームの藤本と申します。

この記事では、女性の月経周期にともなう不調に焦点を当てます。
この時期は調子が良い、この時期は調子が悪いなど、一般に言われる通説がありますが、実際のところはどうなのでしょう。
社内での取り組みを通して可視化できたデータについてご紹介していきます。

女性の月経周期についての通説

女性の正常な月経周期は、一般的に25~38日、そのうち月経期の期間は3~7日間とされています。ただしこれには、個人差があること、ホルモンバランスやストレスによって変化しがちであることが知られています。

月経周期のサイクルは、月経期卵胞期黄体期月経期...という順番で繰り返されます。

このうち、黄体期は精神的・身体的に不調が生じやすい期間です。プロゲステロンというホルモンが増加する影響でむくみが起こったり、情緒不安定になったりします。

一方で卵胞期は、調子が良くなる時期です。卵胞ホルモン(エストロゲン)の分泌が増えることにより、皮膚の活性化が促されて肌艶がよくなったり、自律神経が安定して前向きな気持ちになりやすい期間です。

以上は、女性の月経周期について一般的に言われている通説です。ですが実際にデータを取って解析してみることで、よりくわしく期間ごとの体調の変化をとらえることができます。その一例として、テックドクターで行ったアンケート解析の結果をご紹介します。

体調不良の女性

「Ladynamic」プロジェクトで女性社員にアンケートを実施

テックドクターには、女性社員のみで構成された「Ladynamic」というプロジェクトがあり、女性の視点に立った課題提起とデータ解析を目指しています。

※プロジェクトについてくわしくは、同じデータサイエンスチームの瀬川が書いた記事をご覧ください。
女性にとって、自分の体調が「わかる」未来を目指して〜Ladynamicプロジェクトのご紹介〜 - TechDoctor開発者Blog

Ladynamicではウェアラブルデータ(※)を利用して女性特有のデジタルバイオマーカーの開発に取り組んでいますが、そのプロセスの一環として、プロジェクトに参画している女性社員に日々の体調に関するアンケートに答えてもらっています。

ウェアラブルデータ……スマートウォッチを代表とするウェアラブル端末で測定された生体情報等のデータ

「Ladynamic」アンケートの質問項目

参加者には、下記のようなアンケートが毎日配信されます。質問項目には、日々の体調や気分に関する質問(質問1)と、月経に関する質問(質問2)があります。

質問1(毎日答える質問)

質問内容 選択肢
業務・家事・学業・育児といったタスクに影響がありましたか? 1(全くない)〜4(非常にある)の4段階
余暇時間にリラックスできましたか? 1(全くできなかった)〜4(非常にできた)の4段階
日中の眠気について 1(眠気はまったくない)〜4(非常にある)の4段階
今日の心の調子はいかがでしたか? いつもどおり/いつもと違う気分を感じた(後者の場合は、具体的な症状を選択)
今日の体の調子はいかがでしたか? いつもどおり/いつもと違う異変を感じた(後者の場合は、具体的な症状を選択)
現在、月経期間に該当する はい/いいえ(はいの場合は、質問2に進む)

質問2(月経期に該当する場合のみ表示される質問)

質問内容 選択肢
月経開始から何日目ですか? 1日目〜7日目までの7択から選択
本日の生理痛のつらさについて 1(まったく問題ない) 〜4(かなりつらい)の4段階
本日の経血量について 1(少ない) 〜3(多い)の3段階で評価
月経がはじまる前、体調の変化やつらさを感じた 1(まったく感じなかった)〜4(非常に感じた)の4段階
前回の月経が終了した後、体調の変化やつらさを感じた 1(まったく感じなかった)〜4(非常に感じた)の4段階

スマホでアンケートに回答する女性

参加者数と期間、月経周期の計算方法

アンケートの参加者数は12名、期間は2024年1月1日〜2025年4月24日の約1年5カ月です。

冒頭で触れたように、女性の月経周期には大きく分けて「月経期」「黄体期」「卵胞期」の3つの期間があります。

アンケートの「月経開始から何日目ですか?」の回答をもとに月経期を特定し、黄体期を月経開始前の7日間、卵胞期を月経終了後の5日間としました。

解析結果

1.各期間ごとのからだの不調のようす

まず、期間ごとのからだの不調に関する項目についてその発生数を集計してみました。

グラフ画像
図1: 各期間ごとの全参加者のからだの不調の発生回数

すべての参加者の合計のグラフです。不調の種類ごとに、月経期を赤色、黄体期を黄色、卵胞期を緑色で示しています。発生回数が多いほどグラフが長くなります。
その結果、月経期では「腹痛」が突出して多く、黄体期では「むくみ」や「乳房の腫れ」が多かったです。また、卵胞期では他の期間に比べてからだの不調が少なくなっていました。


グラフ画像

グラフ画像
図2: 各期間ごとのからだの不調の発生回数(参加者ごとの例)

個人ごとのデータも見てみたところ、全体的な傾向と違う人もいました。例えば、参加者4のように月経期にほとんど症状がなく、黄体期に症状が出やすい人や、逆に参加者6のように黄体期にはほとんど症状が出ない人もいました。

2.各期間ごとのこころの不調のようす

次に、期間ごとのこころの不調の項目についても、その発生数を集計してみました。

グラフ画像
図3: 各期間ごとの全参加者のこころの不調の発生回数

すべての参加者の合計のグラフです。月経期では不安・憂鬱などの負の感情や、集中力低下が多く見られました。また、黄体期には、他の時期には見られない「怒り」などの攻撃的な感情が現れたり、「イライラ」の回答数が多くなっていました。
また、卵胞期では他の期間に比べて精神的不調が少なかったです。

グラフ画像

グラフ画像
図4: 各期間ごとのこころの不調の発生回数(参加者ごとの例)

個人ごとのデータも見てみたところ、参加者3のようにまんべんなく症状が出る人もいれば、参加者4のように特定の症状のみが出る人もいました。

3.月経期について

次に、月経期のアンケートの解析結果です。

3-1.生理痛のつらさと経血量について

月経開始からの経過日数でどのように体調が変化していったか見てみましょう。

グラフ画像
図5: 月経開始からの経過日数ごとの、生理痛のつらさの平均スコア(1~4の4段階で評価)

まず、月経開始からの日数ごとに、「本日の生理痛のつらさについて」の4段階の回答の平均をとりました。(グラフの下の数字が経過日数)
その結果、月経開始から2日目で最もつらさを感じている人が多く、月経開始から4日目以降は軽快していく傾向がありました。

グラフ画像
図6: 月経開始からの経過日数ごとの経血量の平均スコア

経血量についても同じように、「本日の経血量について」の3段階の回答の平均をとりました。
その結果、経血量の平均値も生理痛のつらさと同じように月経開始から2日目に最も高くなり、月経開始から4日目以降は軽快する傾向にありました。

3-2.黄体期・卵胞期の不調について

次に、黄体期卵胞期の期間の体調にどのような個人差があったか見てみましょう。

グラフ画像
図7: 参加者ごとの黄体期の体調のつらさの平均スコア(1~4の4段階で評価)

黄体期の体調の変化やつらさに関する質問「月経がはじまる前、体調の変化やつらさを感じた」の4段階の回答の平均をとりました。グラフの下の数字は参加者のIDです。
その結果、黄体期に不調を感じている人が多い一方で、参加者1,9,15のようにほとんど体調のつらさを感じていない人もいました。

グラフ画像
図8: 参加者ごとの卵胞期の体調のつらさの平均スコア(1~4の4段階で評価)

卵胞期の体調の変化やつらさに関する質問「前回の月経が終了したあと、体調の変化やつらさを感じた」の4段階の回答の平均をとりました。
12人中7人は平均スコアが1と、まったく体調の辛さを感じていない人が多いです。一方で、卵胞期においても体調のつらさを感じている人もいました。


まとめ

弊社女性社員のアンケート解析結果でも、月経開始から2日目に生理痛のつらさ・経血量ともに最も高かったこと、卵胞期は他の時期に比べて体調が良い人が多かったことなど、世間的な通説と一致する部分が多い結果となりました。

しかし、全体傾向とはまた違った挙動を示した参加者もおり、全員が同様のつらさを感じているわけではないこともわかりました。全体の解析だけでなく、より個人ごとの解析を進めていく必要があると感じています。

今後の展望

ここまでの解析結果を弊社の女性社員に共有したところ、「人ごとの差を見られて興味深かった」といった感想にくわえ、「春は花粉症や気圧の変化、夏は冷房装置による冷えで調子が悪い、など季節ごとに体調に差がある」「月経期は寝付きが悪くなる」などの意見も出ました。

今後はこれらのフィードバックを元に、Fitbitデバイスのデータを掛け合わせて、生理痛が重い人と軽い人で心拍や睡眠の様子にどのような差が出てくるかなど、より詳しく解析を進めていく予定です。

このように個人ごとのつらさの可視化を行うことで、自分の体調不良の原因の特定や体調不良になりやすい時期の予測が可能になります。
「Ladynamic」プロジェクトでの解析を通して、周囲にそれぞれのつらさを説明しやすい・理解してもらいやすい社会の実現を目指していきたいです。


似顔絵
書いた人:藤本

新規プロダクトを考えるときに役立つ5つのワークショップ~アイデア出しからイメージ共有まで

はじめまして、テックドクターのプロダクトデザイナー、庄司です。テックドクター初のデザイナーとして今年1月に入社し、現在は主に新規プロダクトのUXデザインやUIデザインを行っています。

みなさんの組織では、新しいプロダクトやサービスを考えるとき、どうやってアイデアを出していますか?また、出たアイデアをより具体的にしたり、チーム内でそのイメージを共有するためにどうしていますか?

私はテックドクター入社以前も、大人の女性向けファッションメディア、従業員の心身のケアを行うHR系プロダクト、医療スタートアップにてビデオ診療モジュールのデザイン、医師の勤務のためのアプリ...などなど、さまざまな組織や新規事業において、一人目デザイナーとしてサービスのゼロ→イチの開発に携わってきました。

その経験を踏まえて、この記事ではプロダクトを考え、デザインするためのワークショップを紹介します。

これらはテックドクターでも、現在開発中のプロダクトの立ち上げにあたり実際にチーム内で実施したものです。
今回はオンラインツールのfigjamを用いてハイブリッド形式で行いましたが、ホワイトボードでワイワイやるのも楽しいと思います!


ソリューションを考えるワークショップ

ワークショップ画面
※実際のワークショップのキャプチャのため文字をマスクしています。雰囲気をつかんでいただければ幸いです。

目的

新しいプロダクトを構想するにあたって、まず想定するユーザー(ペルソナ)が抱えている課題や不満(ペイン)を洗い出して明らかにします。
さらに、そのペインに対してどのようなソリューションが考えられるか、アイデアを出し合いながら言語化していくことを目的としています。

手段
  1. まずはペインをガンガン出し、付箋に書いて貼っていく(5分)
  2. 皆で見ながら似たものをグルーピング、余力があったら時系列に並べ替える(15分)
  3. ソリューション案をガンガン出す(5分)
  4. 紐づくペインの付箋の下に、出したソリューション案を動かす。こちらも似てるものはグルーピングしたりする。特に直接的に紐づくものは線で繋ぐとわかりやすい。(15分)
  5. いろいろ話しながら調整したり、追加で思いついたものを足したり、マストっぽいものに印を付けたりなどする。(残った時間)
進め方のコツ
  • ペインを出す上で、一般的なものよりはペルソナに限りなくフォーカスしたものを出すように考えてみる
  • 似たような内容の案も、言葉の表現が違えば解釈が変わり新たなアイデアにつながることもあるので、とにかく書いてみる
やってみて

ふんわりとしたイメージでしかなかったペインが言語化され、具体的な機能を考えていくためのひとまずの指標となりました。
また、着目点が人によって異なることを知ることができました。例えば、Aさんは物事の過程に対してペインが多くあると考える傾向があるのに対し、Bさんは結果としてのペインに注目している、というようなことです。


イデア出しのワークショップ

ワークショップ画面

目的

サービスの方向性をさらにはっきりさせるためのワークショップです。また言葉をたくさん出すことでサービス名のアイディアがひらめくきっかけにもなります。

手段
  1. サービスに関連する言語や類語をたくさん出す(1h前後〜、ワーク前にやっておく)
  2. グルーピングする(5分前後〜、ワーク前にやっておく)
  3. 共有・ディスカッション(30分)
進め方のコツ
  • 事前には類語や例をとにかく沢山出しておく
  • あくまでアイディアをたくさん出すことを目的とする。特に何かを決めることはせず、「この辺りの意味合いは違う気がする」「この単語は好き」など自由にディスカッションし、共通の認識を探るのが目的です。
やってみて

これが直接的にアウトプットにつながるというよりは、サービスを表す言葉・概念を見つけ出す(意識しはじめる)きっかけになったように思います。
そうすることで、次に紹介する「サービスの人格を考える3つのワークショップ」にスムーズに進むことができます。


サービスの人格を考える3つのワークショップ

次にご紹介する3つのワークショップは、過去にコーポレートブランディングの一貫としてやってきたものの応用です。
ブランドを人に例えたとき、「どんな性格なのか」「どんな振る舞いをするのか」を明文化し、人格の方向性を皆で共有するためのワークショップです。
それを今回は企業ではなく、「プロダクトの性格を考える」ためにやってみました。

3つを通した目的

チームメンバーが同じものを見て同じ方向を目指して開発していけるように、まずはプロダクトの世界観をすり合わせます。その中で「このプロダクトって、どんな性格なんだろう?」ということも言葉にしていきながら、チーム全体で共通のイメージを持てるようにします。
3つのワークショップはひとつだけ実施しても効果がありますが、すべて実施するとよりイメージが具体的になります。今回は3つとも実施しました。


ディメンションフレームワークの活用

ワークショップ画面

手段
  1. ブランド(ここではプロダクト)を表す単語を3〜5個書き出す(5分)
  2. 1.で出した単語たちがそれぞれディメンションフレームワーク(誠実/刺激/能力、適性/洗練/頑丈)で定義されている5つの属性のどこに当てはまるかを考える(5分)
  3. 共有・ディスカッション(7分)

※ディメンションフレームワークについてはこちらの記事などを参考にしてください
ブランドの個性を定める – ブランドパーソナリティー【ブランディング入門#5】 - デザイン会社 ビートラックス: ブログ freshtrax


ビッグファイブ理論の応用

ワークショップ画面

手段
  1. ビッグファイブで定義されている5つの属性(外向性/協調性/勤勉性/情動性/創造性の、それぞれどこの性格にプロダクトがあてはまるかをそれぞれ考える(5分)
  2. 共有・ディスカッション(7分)

※ビッグファイブ理論についてはこちらの記事などを参考にしてください
ビッグファイブとは?5つの性格特性と心理テストを紹介 | 社員研修のアチーブメントHRソリューションズ



アーキタイプフレームワークの活用

ワークショップ画面

手段
  1. プロダクトがアーキタイプフレームワークで定義されている属性の、どこに存在するのが理想的かを考え、一人5票を投票する。一番ふさわしいと思う箇所に5票全て入れてもいいし、ばらけてもいい(7分)
  2. 共有・ディスカッション(7分)

アーキタイプフレームワークについてはこちらの記事などを参考にしてください。(ディメンションフレームワークのところで挙げた参考記事と同じです。両方について書かれています)
ブランドの個性を定める – ブランドパーソナリティー【ブランディング入門#5】 - デザイン会社 ビートラックス: ブログ freshtrax

3つをやってみて

共有・ディスカッションの時間には、おのおの「自分がどうしてそこの場所を選んだのか」を共有しました。そうすることで「それぞれが勝手に描いているプロダクト像」が「みんなの共通の知り合いのイメージ(=プロダクト像)」になっていくような感覚を得られました。
結果的には大まかなイメージに相違がないことが確認できましたが、それと同時に、細部で違う部分があることも可視化されました。そこに対して突っ込んで話を聞いていくうちに、個々の思い入れやこだわり、好みの違いが見られて興味深かったです。

最後に

最後に、これらのワークショップ全体を通して大事にしていたことを紹介します。ワークショップの前に以下の2つを参加者に伝えました。

  • 一度書いてみて、これはナシかな〜と思っても削除せず、思考の発露としてどこかに場所を作って取っておく(後で何かのヒントになるかわからない、自分はイマイチと思っても他人にとっては気付きだったりする、等の理由)
  • 発言数が多いからえらい、少ないからダメとかいうことではない。参加することそのものが大事!ということ

プロダクト開発の最初期、その方向性やサービスを考え決めていく段階で、具体的にどんなことをすればいいのかという情報はあまり無い気がするので、今回記事にしてみました。

参考になったら幸いです。

似顔絵
書いた人:庄司

API シナリオテストツール Postman・Tavern・runn 徹底比較 – 私が runn を選んだ理由

はじめに

はじめまして、テックドクターでバックエンドエンジニアをしている筧と申します。

最近、弊社では API の品質を担保するために「API シナリオテスト」をプロダクトに導入しました。今回は、この API シナリオテストのツールである Postman(+Newman)、Tavern そして runn を比較し、最終的に runn を選んだ理由をご紹介します。

API シナリオテストとは?

API シナリオテストとはなんでしょうか?

開発におけるテストといえば、ユニットテスト結合テストAPI テストや E2E テストなどをよく耳にします。しかしAPI シナリオテストという言葉はあまり聞き馴染みがないという方も多いかもしれません。

API シナリオテストは API テストの一種で、複数の API を連鎖的に呼び出して実行するテストです。以下の特徴を持っています。

  • 複数の API を順序立てて呼び出し、一連のフローを検証する
  • 前のAPI呼び出しのレスポンスデータを次のリクエストに引き継ぐ
  • UI テストや E2E テストと比べ、実行スピードが早い

シナリオの例として、

  • SNSの場合… ユーザー登録 → メール認証 → ログイン → プロフィール更新
  • 通販サイトの場合… 商品検索 → カートに投入 → 注文確定

などが挙げられます。

API シナリオテストツールの概要

改めて、今回検証したツールは下記の3つです。

  • Postman (+Newman)
  • Tavern
  • runn

まずは一覧表で各ツールの特徴をご紹介します。

項目     Postman
(+Newman)
Tavern runn
用途 API 開発、テスト、モニタリング、ドキュメント作成 テストの自動化、API のシナリオベースのテスト テストの自動化、API のシナリオベースのテスト
操作方法 GUI ベース YAML ベース YAML ベース
スクリプト
言語
JavaScript YAML + Python YAML + Go
テスト実行 Newman で CLI 実行 pytest で実行 runn コマンドで実行
拡張性 プラグインスクリプトで対応可能 Python ヘルパー利用可能 Go ヘルパー、
DB アクセス可能
レポート機能 GUI/ダッシュボード pytest レポート octocov 等利用
チーム
機能
ワークスペース共有化 YAML 管理 YAML 管理
価格 無料~有料 無料 無料


それぞれ、もう少し詳しく見ていきましょう。

Postman (+ Newman)

概要

Postman は、API に関する設計や運用・テストなどを全般的に行える API プラットフォームです。
GUI ベースで API リクエストの作成・送信・レスポンスの検証が行えます。
また、CLI ツールである Newman を利用することで、 Postman で作成したコレクションを CI/CD パイプライン上で自動実行することができます。

特徴
  • 直感的なGUI による API 設計・テスト・デバッグが可能
  • Newman を活用した CI/CD パイプラインとの親和性
  • 変数・環境 (Postman Environment) を使ったシナリオテストや E2E テストが容易
  • APIドキュメントやモックサーバー作成などの幅広い機能を提供
  • 無料版の機能が充実しており、導入ハードルが低い

Tavern

概要

Tavern は、YAML で記述する API 自動テストツールです。
設定ファイルを用いてコードとして API テストを定義・管理できるため、テストの自動化やバージョン管理が容易にできます。

特徴
  • YAML によるテストシナリオ定義で、コードレビューや管理が容易
  • Python ベースのため、テストのカスタマイズや拡張性に優れている
  • レスポンスデータの検証や複雑なシナリオテストに対応
  • Git によるコード管理ができ、チーム開発との相性が良い

runn

概要

runn はAPI シナリオテストに特化したツールです。Tavern 同様 YAML でテストシナリオを記述できるほか、Go ヘルパーの利用やDBの直接アクセスも可能で、柔軟性の高いテストを書くことができます。

特徴
  • シナリオテストに特化した設計で、複数 API を連携させた一連のフローを検証可能
  • 変数やレスポンスデータの動的な参照・更新が容易
  • YAML によるシナリオ記述や Go ヘルパーを利用でき、柔軟性が高い
  • 軽量で高速な実行が可能であり、実行パフォーマンスが高い
  • CI/CD ツールとの統合が簡単で、自動化テスト環境の構築に適している
  • PostgreSQLMySQL・SQLite3・Cloud Spanner のデータベースにアクセスでき、クエリを実行できる

各ツールを触ってみる

では実際に各ツールを検証していきます。

テスト対象APIサーバの準備

テスト対象として、Full Stack FastAPI Template というプロジェクトの API サーバーを建てることにします。

git clone git@github.com:fastapi/full-stack-fastapi-template.git
cd full-stack-fastapi-template

サーバを起動します。

docker compose up --build

DB なども同時に立ち上がります。
立ち上げた API サーバー (http://localhost:8000) について、次のシナリオをテストしたいと思います。

ユーザ登録〜ログイン〜プロフィール取得

  1. ユーザ登録(Register User)
  2. ログイン(Login Access Token)
  3. プロフィール取得(Read User Me)

①Postman + Newman

Postman のインストール

Download Postman から Postman をインストールできます
Postman の設定で、自動保存を有効にしています
画面キャプチャ

また、Newman を使うときに Node.js を使うため、 node コマンドと npm コマンドを使用できるようにします

❯ node -v
v23.11.0

❯ npm -v
10.9.2

Postmanの設定

1. コレクション情報のインポート

http://localhost:8000/api/v1/openapi.json の内容をもとに、コレクションをインポートします。
コレクション タブの上部にある インポート からインポートできます (下図を参照)。

画面キャプチャ

2. 環境変数の登録

Postman のサイドバーから 環境 のタブを選択して、環境を作成します。

Local という名前で環境(Postman Environment)を作成しました。ここに環境変数を登録します。

変数 タイプ 初期値 現在値
baseUrl デフォルト http://localhost:8000 http://localhost:8000
email デフォルト
password シークレット
access_token シークレット
3. 各テストスクリプトの準備

実行したいテストのスクリプトを書いていきます。API のリクエストと同時に、 API 単体テストも同時に行ってくれるようです。

①ユーザ登録(Register User: POST /api/v1/users/signup

  • リクエストボディ (Raw)
{
  "email": "user{{$timestamp}}@example.com",
  "password": "Passw0rd!",
  "full_name": "user{{$timestamp}}"
}

 

pm.test("登録ステータスは200", () => {
  pm.response.to.have.status(200);
});

const user = pm.response.json();
pm.test("ユーザIDが返る", () => {
  pm.expect(user).to.have.property("id");
});

// 環境変数に登録情報を保存
pm.environment.set("email", user.email);
pm.environment.set("password", "Passw0rd!");

書けたら、動作確認のためにAPIリクエストを実行してみます。

  • レスポンス

画面キャプチャ

  • テスト結果

画面キャプチャ

同じように、ログイン(Login Access Token)とプロフィール取得(Read User Me)も設定します。

②ログイン(Login Access Token: POST /api/v1/login/access-token

  • リクエストボディ (x-www-form-urlencoded)
キー
username {{email}}
password {{password}}
grant_type password
scope
client_id
client_secret

 

pm.test("ログインステータスは200", () => {
  pm.response.to.have.status(200);
});

const result = pm.response.json();
pm.test("access_token が返る", () => {
  pm.expect(result).to.have.property("access_token");
});

// トークンを次リクエスト用に保存
pm.environment.set("access_token", result.access_token);

 

③プロフィール取得(Read User Me: GET /api/v1/users/me

  • リクエストボディ: なし

 

 

pm.test("取得ステータスは200", () => {
  pm.response.to.have.status(200);
});

const profile = pm.response.json();
pm.test("email が一致する", () => {
  pm.expect(profile.email).to.eql(pm.environment.get("email"));
});
4. コレクションと環境のエクスポート

Newman 経由で実行するために、必要な情報をエクスポートしておきます。

  • コレクションv2.1 で full-stack-fastapi-template.postman_collection.json という名前でエクスポート
  • Local 環境を local.postman_environment.json という名前でエクスポート

これで Postman の設定は以上です。次はNewman の設定に移ります。

Newman の設定

1. プロジェクト用ディレクトリの作成

テストをするためのディレクトリを作成して、そこに先ほど作成した

  • full-stack-fastapi-template.postman_collection.json
  • local.postman_environment.json

を置きます。

2. シナリオに合わせてコレクションJSONを並び替え

今回はコレクションJSONを openapi からインポートしたため、テスト対象のエンドポイントがシナリオ通りの順番になっていません。コレクション JSON を並び替えるスクリプトを作成しました。
※コレクションを最初から実行順に記述する、pm.execution.setNextRequest()を使う、Postman Flowsを利用するなどの方法もあります。

// reorder-collection.js
const fs = require('fs');
const path = require('path');

// 元のコレクションファイル名と出力ファイル名
const SRC_FILE = 'full-stack-fastapi-template.postman_collection.json';
const OUT_FILE = 'scenario_collection.json';

const desiredPaths = [
  'api/v1/users/signup',           // Register User
  'api/v1/login/access-token',     // Login Access Token
  'api/v1/users/me',               // Read User Me
  // 他のシナリオが増えたら、パス文字列を追加していく
];

const col = JSON.parse(fs.readFileSync(path.resolve(__dirname, SRC_FILE), 'utf8'));

// ネストされた item 配列を再帰的にフラット化して { path, item } のリストを作成
function collectEndpoints(items, result = []) {
  items.forEach(i => {
    if (i.request && i.request.url && Array.isArray(i.request.url.path)) {
      const p = i.request.url.path.join('/');
      result.push({ path: p, item: i });
    }
    if (i.item) {
      collectEndpoints(i.item, result);
    }
  });
  return result;
}

const flatList = collectEndpoints(col.item);

// desiredPaths の順に対応する request オブジェクトを抜き出す
const orderedItems = desiredPaths.map(p => {
  const found = flatList.find(f => f.path === p);
  if (!found) {
    console.warn(`⚠️ Path not found: ${p}`);
    return null;
  }
  return found.item;
}).filter(Boolean);

// 新コレクションを構築(必要に応じて name や info を調整してください)
const newCollection = {
  info: {
    name: col.info.name + ' - Scenario',
    schema: col.info.schema,
    _postman_id: col.info._postman_id
  },
  item: [
    {
      name: 'Custom Scenario',
      item: orderedItems
    }
  ]
};

fs.writeFileSync(
  path.resolve(__dirname, OUT_FILE),
  JSON.stringify(newCollection, null, 4),
  'utf8'
);

console.log(`✅ ${OUT_FILE} generated with ${orderedItems.length} requests.`);

 

node 環境を作成して reorder-collection.js を実行します。

npm init -y
npm install
npm install --save-dev newman  # newman をインストール
node reorder-collection.js  # reorder-collection.js を実行
3. API シナリオテストの実行

newman を実行し、実際にAPI シナリオテストを行います。

npx newman run scenario_collection.json -e local.postman_environment.json

 

結果

❯ npx newman run scenario_collection.json -e local.postman_environment.json
newman

Full Stack FastAPI Project - Scenario

❏ Custom Scenario
↳ Register User
  POST http://localhost:8000/api/v1/users/signup [200 OK, 275B, 230ms]
  ✓  登録ステータスは200
  ✓  ユーザIDが返る

↳ Login Access Token
  POST http://localhost:8000/api/v1/login/access-token [200 OK, 332B, 184ms]
  ✓  ログインステータスは200
  ✓  access_token が返る

↳ Read User Me
  GET http://localhost:8000/api/v1/users/me [200 OK, 275B, 4ms]
  ✓  取得ステータスは200
  ✓  email が一致する

┌─────────────────────────┬───────────────────┬───────────────────┐
│                         │          executed │            failed │
├─────────────────────────┼───────────────────┼───────────────────┤
│              iterations │                 10 │
├─────────────────────────┼───────────────────┼───────────────────┤
│                requests │                 30 │
├─────────────────────────┼───────────────────┼───────────────────┤
│            test-scripts │                 30 │
├─────────────────────────┼───────────────────┼───────────────────┤
│      prerequest-scripts │                 30 │
├─────────────────────────┼───────────────────┼───────────────────┤
│              assertions │                 60 │
├─────────────────────────┴───────────────────┴───────────────────┤
│ total run duration: 487ms                                       │
├─────────────────────────────────────────────────────────────────┤
│ total data received: 504B (approx)                              │
├─────────────────────────────────────────────────────────────────┤
│ average response time: 139ms [min: 4ms, max: 230ms, s.d.: 97ms] │
└─────────────────────────────────────────────────────────────────┘

これで Postman + Newman でシナリオテストを実行できました。

②Tavern

1. プロジェクトの初期化

poetry を使ってプロジェクトを作成します。

# プロジェクト用ディレクトリを作成して移動
mkdir tavern-tests && cd tavern-tests

# Poetry プロジェクトを初期化
poetry init --no-interaction \
  --name tavern-tests \
  --dev-dependency "pytest@^7.0" \
  --dev-dependency "tavern[pytest]"

# 依存関係をインストール
poetry install --no-root

このままテストするとwarningが出てしまったので、作成された pyproject.toml に以下を追記して PytestDeprecationWarning を無視するようにします。

[tool.pytest.ini_options]
filterwarnings = [
    "ignore::pytest.PytestDeprecationWarning"
]
2. ディレクトリの作成

今回はこのようにしました。

tavern-tests/
├── pyproject.toml
└── tests/
    ├── conftest.py
    └── test_user_flow.tavern.yaml
3. テストファイルの作成

変数は tavern ファイル側で定義することもできますが、今回は conftest.py で定義してみました。シナリオ定義はYAMLで作成します。

  • tests/conftest.py
# tests/conftest.py
import datetime
import pytest

@pytest.fixture(scope="session")
def base_url() -> str:
    return "http://localhost:8000"

@pytest.fixture
def timestamp() -> int:
    return datetime.datetime.now().timestamp()

@pytest.fixture
def email(timestamp) -> str:
    return f"user{timestamp}@example.com"

@pytest.fixture
def password() -> str:
    return "Passw0rd!"

@pytest.fixture
def full_name(timestamp) -> str:
    return f"user{timestamp}"

 

  • tests/test_user_flow.tavern.yaml:シナリオ定義
# tests/test_user_flow.tavern.yaml
test_name: "ユーザ登録〜ログイン〜プロフィール取得"

marks:
  - usefixtures:
      - base_url
      - email
      - password
      - full_name

stages:
  - name: ユーザ登録 (Register User)
    request:
      method: POST
      url: "{base_url}/api/v1/users/signup"
      headers:
        Content-Type: application/json
      json:
        email: "{email}"
        password: "{password}"
        full_name: "{full_name}"
    response:
      status_code: 200
      json:
        id: !anystr
        email: "{email}"
        full_name: "{full_name}"
        is_active: true
        is_superuser: false
      save:
        json:
          user_id: id

  - name: ログイン (Login Access Token)
    request:
      method: POST
      url: "{base_url}/api/v1/login/access-token"
      headers:
        Content-Type: application/x-www-form-urlencoded
      data:
        username: "{email}"
        password: "{password}"
        grant_type: "password"
    response:
      status_code: 200
      json:
        access_token: !anystr
        token_type: "bearer"
      save:
        json:
          access_token: access_token

  - name: プロフィール取得 (Read User Me)
    request:
      method: GET
      url: "{base_url}/api/v1/users/me"
      headers:
        Authorization: "Bearer {access_token}"
    response:
      status_code: 200
      json:
        id: "{user_id}"
        email: "{email}"
        full_name: "{full_name}"
        is_active: true
        is_superuser: false
4. テストの実行
poetry run pytest

 

  • 結果
❯ poetry run pytest
=================== test session starts ====================
platform darwin -- Python 3.13.2, pytest-7.2.2, pluggy-1.5.0
rootdir: ../tavern-tests, configfile: pyproject.toml
plugins: tavern-2.15.0
collected 1 item                                           

tests/test_user_flow.tavern.yaml .                   [100%]

==================== 1 passed in 0.52s =====================

 

Tavern でのテストができました。

runn

1. runn のインストール

mac を使っているので brew からインストールします

brew install runn

他にも go や docker からもインストールできるみたいです: runnをインストールする

2. プロジェクトの初期化

プロジェクト用ディレクトリを作成します。

mkdir runn-tests && cd runn-tests
3. テストするシナリオを書く

YAMLで記述します。user_flow.yaml を作成しました。

# user_flow.yaml
desc: ユーザ登録〜ログイン〜プロフィール取得

runners:
  req:
    endpoint: http://localhost:8000
    openapi3: http://localhost:8000/api/v1/openapi.json
    skipValidateRequest: false
    skipValidateResponse: false
  db: postgres://postgres:changethis@localhost:5432/app?sslmode=disable

vars:
  password: "Passw0rd!"

steps:
  - desc: タイムスタンプを取得
    bind:
      timestamp: now().Unix()

  - desc: ユーザ登録 (Register User)
    req:
      /api/v1/users/signup:
        post:
          headers:
            Content-Type: application/json
          body:
            application/json:
              email: "user{{ timestamp }}@example.com"
              password: "{{ vars.password}}"
              full_name: "user{{ timestamp }}"
    test: |
      current.res.status == 200
      && current.res.body.id != ""
    bind:
      user_id: current.res.body.id
      email: current.res.body.email
      full_name: current.res.body.full_name

  - desc: ログイン (Login Access Token)
    req:
      /api/v1/login/access-token:
        post:
          headers:
            Content-Type: application/x-www-form-urlencoded
          body:
            application/x-www-form-urlencoded:
              username: "{{ email }}"
              password: "{{ vars.password }}"
              grant_type: "password"
    test: |
      current.res.status == 200
      && current.res.body.access_token != ""
      && current.res.body.token_type == "bearer"
    bind:
      access_token: current.res.body.access_token

  - desc: プロフィール取得 (Read User Me)
    req:
      /api/v1/users/me:
        get:
          headers:
            Authorization: "Bearer {{ access_token }}"
    test: |
      current.res.status == 200
      && current.res.body.id == user_id
      && current.res.body.email == email
      && current.res.body.full_name == full_name

  - desc: データベースの確認
    db:
      query: |
        SELECT email, full_name
        FROM "user"
        WHERE email = '{{ email }}';
    test: |
      len(current.rows) == 1
      && current.rows[0].email == email
      && current.rows[0].full_name == full_name

 

4. テスト実行

次のコマンドで実行できます。

runn run user_flow.yaml

各ステップごとの実行結果を確認したいので、今回は --verbose オプションをつけてみます。

❯ runn run user_flow.yaml --verbose
=== ユーザ登録〜ログイン〜プロフィール取得 (user_flow.yaml)
    --- タイムスタンプを取得 (0) ... ok
    --- ユーザ登録 (Register User) (1) ... ok
    --- ログイン (Login Access Token) (2) ... ok
    --- プロフィール取得 (Read User Me) (3) ... ok
    --- データベースの確認 (4) ... ok

1 scenario, 0 skipped, 0 failures

runn によるテストができました。

runn を採用した理由

もともと、弊社のプロダクトでは次のような前提や要望がありました

前提

  • APIPython の FastAPI で作成している
  • Postman は今後、シナリオテスト以外の用途でも使用予定

要望

  • API のシナリオをテストしたい
  • コードベースで Git 管理がしたい
  • 継続的に開発できるようにしたい

上記を踏まえたうえで、各ツールを実際検証して比較した結果がこちらです。

Postman + Newman

  • メリット
    • 今後プロダクトで使用する可能性が高く、テストとのシナジーが大きい
    • Postman Flows による視認性の高いシナリオテストが可能
    • コミュニティが広く、継続的な開発が期待できる
  • デメリット
    • 複雑なシナリオ(条件分岐やループ処理)のテストは難しい
      • Postman Flows である程度解決可能
      • Newman 側で調整もできるが、追加の準備が必要
    • コードベースでの管理には別途準備が必要
    • Postman Flows 自体はコード管理に対応していない

Tavern

  • メリット
    • APIPython 実装のため、pytest との相性が良い
    • 単体テストとシナリオテストを統合して一括実行可能
    • シナリオを Git 管理できる
  • デメリット
    • 最新の pytest (v8 以降) に対応しておらず、バックエンドリポジトリ内でシナリオテストが記述できない
    • 継続的な開発が行われるか不透明
    • !anything などの特殊構文が YAML リンターエラーの原因となる

runn

  • メリット
    • 要件をすべてクリアしている
    • 式の評価に expr-lang を使用し、変数定義やアサーションを柔軟に記述可能
    • --debug フラグや DB へのアクセスが可能で、デバッグが容易
  • デメリット
    • 目立ったデメリットは特になかった。しいて言えば…
    • runn や expr-lang の特有の機能を使いこなすのに慣れが必要
      • 例えば、 dump (print 文のようなもの) の機能で dump: "Hello World!" としたらエラーが出て、 dump: "'Hello World!'" だとうまくいくなど
    • expr-lang が使えるところ・使えないところがある
      • 例えば、 vars の中で基本的に expr-lang は使えないが、 parent を使えば使えるなど


Postman は GUI ベースで直感的に使える一方、複雑なシナリオやコードベースでの管理には追加の工夫が必要でした。Tavern は Python・pytest との親和性が高く、コード管理や自動化に強みがあるものの、pytest のバージョン制約や継続的な開発面で課題が残りました。

一方で runn は API シナリオテストに特化した設計・柔軟な変数やアサーション記述・DB アクセスやデバッグ機能など、現場の要件をすべて満たし、かつ継続的な開発が期待できるツールだと感じました。YAML ベースで Git 管理しやすく、CI/CD との統合も容易でした。

以上を考慮して、今回はrunnを導入することに決めました。

導入してよかったこと

3月より実際にプロダクトに runn を使った API シナリオテストを CI/CD 込みで導入しました。
導入して2ヶ月使ってみた上で、さらに以下のような良かった点がありました。

API のバグを発見
開発時を含め、 API のバグを 3 件以上発見することができました

needs 機能
トークン取得 API を毎回リクエストする必要がなくなり、テスト実行速度と可読性が向上しました

--debug フラグ
テスト実行中にどのステップまで進んだか即座に把握でき、シナリオ作成時のデバッグが非常に捗りました

CDP(Chrome DevTools Protocol)対応
ブラウザ操作を自動化でき、ブラウザでの認証フローをスムーズに実施できました

includes によるランブック再利用
テストシナリオをモジュール化し、共通手順を簡単に使い回すことができました

DB への直接アクセス
GET API の fixture や DELETE APIアサーション時に、DB に直接クエリを投げられるため、結果検証と後処理が簡単にできました

expr-lang による式評価
YAML ファイル内で計算や文字列操作などの式が完結し、Python ヘルパー不要で柔軟にテストを書くことができました

まとめ

本記事では、API シナリオテストの概要と、Postman(+Newman)、Tavern、runnという 3 つの主要ツールの特徴・使い勝手を実際にシナリオテストを行って比較し、最終的にrunnを導入するまでの経緯について書きました。

実際に導入したことで、テストの可読性・保守性・実行速度が向上し、開発フローの品質担保に大きく貢献しています。今後も API 品質向上のため、 API シナリオテストの継続的な活用と改善を進めていきたいと考えています。

Google ColabでRによるベイズ推定を行う方法: 睡眠リズムの定量化を例に

こんにちは。データサイエンスチームの坂本と申します。

使い慣れたRを使って、Google Colabのクラウド環境上でベイズ推定ができたら便利ですよね。しかしやってみると意外に環境設定手順が複雑で、悩むことになるかもしれません。
TechBlog第15回では、統計解析環境Rのユーザーが、Google Colab上でベイズ推定を行う際の手続きを紹介します。

ベイズ推定にはハミルトニアンモンテカルロ法のNUTSアルゴリズムを使用してパラメータ推定を行うためのRパッケージ、cmdstanrを利用します。

また、せっかくですので、後半では構築した環境を使った分析例もご紹介します。ウェアラブル端末で測定した睡眠データで「睡眠リズムの安定性(不安定性)」を定量化してみましょう。睡眠リズムを定量化する方法の意外な難しさと、時刻データの解析に関するちょっと面白いお話も披露できたらと思います。

※2025年3月時点で適正動作が確認できた方法を紹介しています。

Google Colabでcmdstanrのインストール

まずは環境設定です。Google Colabに、統計解析環境Rのパッケージである”cmdstanr”をインストールし、動作確認を行います。
なお、作業前に、Google Driveのマイドライブ直下に、「library」という空のフォルダを作成しておきましょう。

1. [ランタイム: python] Google Driveをマウント

#@title Python3: Google Driveのマウント


# content/driveをマウント
from google.colab import drive
drive.mount('/content/drive/')


2. [ランタイム: R] ライブラリ(パッケージ)のインストール先を設定

# Rのライブラリを管理するフォルダのパスを追加 
.libPaths("/content/drive/MyDrive/library")


3. [ランタイム: R] cmdstanrパッケージのインストール

#@title Installation "cmdstanr"

# 最初の一回だけ実行
install.packages("cmdstanr", repos = c("https://mc-stan.org/r-packages/", getOption("repos")))


4. [ランタイム: R] ライブラリの読み込みとcmdstanrの動作環境整備
少し時間がかかりますが、粘り強く待ちましょう。

# cmdstanr
library(cmdstanr)
# 使う時に実行(接続した環境にinstallする必要がある)
cmdstanr::install_cmdstan(dir = "/content/drive/MyDrive/library", overwrite = TRUE)


5. [ランタイム: R] cmdstanrの保存パス確認とパス設定
保存されたcmdstanrのパスとバージョンの確認

#@title Unpacked Directory
cmdstanr::cmdstan_path()


使用するcmdstanrのパスとバージョン設定

#@title Path Setting for CmdStanR
set_cmdstan_path(path = "/content/drive/MyDrive/library/cmdstan-2.36.0")


6. [ランタイム:R] 動作確認: 統計モデルの作成(.stanファイル)
ここでは、正規分布の平均と標準偏差を推定するサンプルモデルを例示

#@title Stan Model and Save as .stan file

ModelScript = "
 data {
   int N;
   vector[N] Y;
 }
 parameters {
   real mu;
   real<lower=0> sigma;
 }
 model {
   Y ~ normal(mu, sigma);
 }

"

# save as .stan file
writeLines(ModelScript, "作業ディレクトリのパス/normal.stan")


サンプルデータの準備とモデルのコンパイル

#@title Sample Data & Compile
datastan = list(
 N = 50,
 Y = rnorm(50, 0, 1)
)

# model compilation
cmd_model = cmdstan_model("作業ディレクトリのパス/normal.stan")

# model script
cmd_model


7. [ランタイム: R] パラメータ推定と結果の確認(収束診断と推定値)

#@title Sampling(Parameter Estimation) and Results


# パラメータ推定
fit <- cmd_model$sample(
 data = datastan,
 seed = 123,
 chains = 4,           # チェーン数
 parallel_chains = 4,  # 並列処理
 iter_sampling = 1000, # サンプリング回数
 iter_warmup = 500     # ウォームアップ期間
)

#収束診断 (Stan Recommendation Rhats < 1.05)
all(fit$summary()[, 'rhat'] < 1.05)

# 結果の確認
fit$summary()


ここまで、問題なく実行できたでしょうか。STEP4のcmdstanrの動作環境整備では、g++のインストールに時間がかかったのではないかと思います。
現在のGoogle Colabでは、cmdstanrを利用するたびに毎回g++のインストールを実行する必要があるため、不便を感じるところかもしれません。

循環正規分布(von Mises distribution)を用いた睡眠中央時刻の平均と分散の推定

さて、cmdstanrが使えるようになったところで、実際に推定を行ってみます。
ベイズ推定法の利用場面に相応しい、「睡眠リズムの安定性(不安定性)」の定量化を行ってみます。

「睡眠リズムがバラバラ」という状態は、体験的にはイメージしやすいかもしれません。しかし、いざデータ解析で「バラバラ度合い」を定量化しようとすると、悩ましい問題が出てくることに気がつきます。

一つの問題は、「睡眠時間」「入眠時刻」「起床時刻」、あるいは「1日の睡眠の(分断)回数」などのうち、どの情報のバラバラ度合いを計算すれば良いのかという問題です。

もう一つは、23時と0時の就寝では時刻の違いは1時間ですが、数値の上では0(最小値)と23(最大値)で大きく乖離してしまう「循環データ」の問題です。0時と23時のバラバラ度合いは実際には1時間ですが、数値上は23時間のようになってしまうというものです。

これら問題にはいくつかの対処方法が考えられますが、本記事では「睡眠中央時刻のバラつき度合い」を、循環データに対応した「循環正規分布(von Mises distribution)を用いて推定する」という方法をご紹介します。

睡眠中央時刻のバラつき

この記事では、「入眠した時刻から覚醒した時刻の中間の時刻」を睡眠中央時刻と呼びます。22時に就寝して6時に起床した場合は2時、0時に就寝して7時に起床した場合は3時半です。

規則正しい生活を送っている場合、睡眠中央時刻は特定の時刻付近に集中します。反対に、睡眠リズムが不規則な場合には、様々な時刻に睡眠中央時刻が現れます。1周が24時間の「24時間時計」を用いて、2名の成人男性の1年間の睡眠中央時刻の分布を見てみましょう(図1)。

※いずれも測定端末はGoogle Fitbit(ID=1はCharge5、ID=2はCharge6を装着)。

グラフ画像
図1 24時間時計(円周)上の睡眠中央時刻の分布(昼寝等を含む約1年間の睡眠データより)

循環正規分布を用いたパラメータ推定

図1に示した2名の睡眠中央時刻データを利用し、それぞれの睡眠中央時刻の円周上の平均と分散パラメータを推定します。von Mises distributionとstanモデルの導入解説は、下記のブログ記事に大変わかりやすく整理されていますので、ご参照ください。

bayesmax.sblo.jp

睡眠中央時刻は00:00:00が0度、12:00:00が180度となるようあらかじめ時刻を角度に変換し、角度に対してπ/180を乗算することでラジアンも計算しておきました。stanモデルは次の通りです。なお、データブロックに記載されたradian_lowerとradian_upperは、実際の睡眠中央時刻から計算された観測データ(ラジアン)の25%タイル点と75%タイル点です。

#@title von-Mises Difference.stan

ModelScript = "
 //vonMises_difference.stan
 data {
   int n1;
   int n2;
   vector[n1] radian1;
   vector[n2] radian2;
   real radian1_lower;
   real radian1_upper;
   real radian2_lower;
   real radian2_upper;
 }
 parameters {
   real<lower=radian1_lower, upper=radian1_upper> mu1;
   real<lower=radian2_lower, upper=radian2_upper> mu2;
   vector<lower=0, upper=200>[2] kappa;
 }
 transformed parameters {

 }
 model {
   for (n in 1:n1){
     radian1[n] ~ von_mises(mu1, kappa[1]);  //群1のモデル
   }
   for (m in 1:n2){
     radian2[m] ~ von_mises(mu2, kappa[2]);  //群2のモデル
   }
 }
 generated quantities {
   real mu1_angle;  //mu1(ラジアン)を度数に変換
   real mu2_angle;  //mu2(ラジアン)を度数に変換
   vector[2] a;  //平均合成ベクトル長(円周分散・円周標準偏差の計算に使用)
   vector[2] v;  //群1と群2の円周分散
   vector[2] nu;  //群1と群2の円周標準偏差
   vector[2] nu_angle;  //nu(ラジアン)を度数に変換
   real mu_angle_diff;  //mu1_angleとmu2_angleの差(最短距離)
   real diff_nu; //nu1とnu2の差
   real diff_mu_angle; //nu1とnu2の差

   // feature calculation
   for (k in 1:2){
     a[k] = modified_bessel_first_kind(1,kappa[k])/modified_bessel_first_kind(0,kappa[k]);
     v[k] = 1 - a[k];
     nu[k] = sqrt(-2 * log(a[k]));
     nu_angle[k] = nu[k] / pi() * 180;
   }
   mu1_angle = mu1 / pi() * 180;
   mu2_angle = mu2 / pi() * 180;
   mu_angle_diff = mu2_angle - mu1_angle;
   diff_nu = nu[2] - nu[1];
   diff_mu_angle = mu1_angle - mu2_angle;
 }

"

推定された結果を確認してみましょう。chains = 4、iter_sampling = 2000、iter_warmup = 1000、max_treedepth = 10、thin = 1、adapt_delta = 0.8の条件で、全てのパラメータの収束(Rhat統計量 < 1.05)を確認しています(図2)。

グラフ画像
図2 循環正規分布(von Mises distribution)を用いた睡眠中央時刻の円周上平均と分散パラメータの推定値(ヒストグラムのエラーバーは±1SDを示し、棒グラフのエラーバーは分散パラメータの95%信用区間を示す)

パラメータ推定の結果、睡眠が不規則なID=1の対象者では、睡眠中央時刻の円周上平均(μ)が05:11ごろとなり、バラつき指標(ν)は5.01(単位: hour)となりました。また、規則的な生活を送っているID=2の対象者は、睡眠中央時刻の円周上平均(μ)が02:04ごろとなり、バラつき指標(ν)が0.67(単位:hour)となりました。ID=1の対象者の方が寝ている時間が夜間遅い方向にあり、寝ている時間帯がID=2よりもバラついている、つまり「睡眠リズムがバラバラ」ということを示す結果となっています。

おわりに

この記事では、「解析環境の設定」と「時刻データの統計学的処理方法」を紹介しました。データ解析者にとっては悩ましい二つのポイントへの処方箋となれば嬉しく思います。

また、長期間にわたって記録された睡眠データの可視化と解析結果に、面白さを感じていただけたら幸いです。


似顔絵
書いた人:坂本

プロジェクト管理ツールLinearの紹介~プロジェクト管理の手間を減らし、開発に集中できる環境へ

こんにちは。テックドクターでプロダクトマネージャーをしている田向です。

テックドクターでは2023年からプロジェクト管理ツールとしてLinearを導入しています。

linear.app

一言で言えば、Linearは「プロジェクト管理の手間を最小化し、顧客に価値を提供するための開発に集中できるツール」です。

プロジェクト管理は大事ですが、管理することに時間を取られて、肝心の開発に手が回らなくなってしまっては本末転倒です。

本記事ではLinearの概要と、個人的にLinearを使用する最大のメリットと感じているGitHub連携やSlack連携の機能を中心にリアルな使用感を紹介していきます。

「もっと使いやすくてシンプルなプロジェクト管理ツールが欲しい」
「プロジェクト管理作業の時間を減らして開発に集中したい」
そんな方に、この記事が少しでも参考になれば嬉しいです。

Linearの概要

Linearは、チームのプロジェクト管理とタスク追跡を効率化するためのツールです。

シンプルで直感的なUI、動作の軽快さが特徴で、タスクの作成、割り当て、進行状況の追跡を簡単に行え、プロジェクトの進捗状況を一目で把握できます。

画面キャプチャ
プロジェクトのIssue一覧画面です。画面に表示される情報量が多く、カスタマイズ性も高いため必要な情報を効率的に把握できます。(linear.appより引用)

新しいツールを導入するときによくある「学習コストの高さ」や「設定の複雑さ」もほとんどなく、誰でもすぐに使いはじめられるのも大きな魅力の1つです。

続いて、Linearの最大のメリットであるGitHub連携やSlack連携の機能を見ていきましょう。

GitHub連携

Linearでは、GitHubのPull Request(以下、PR)とLinearのIssueを紐付ける機能が提供されています。

例えば、Issueキー(例:HOGE-23)を含んだブランチ(例:feature/hoge-23)を作成し、そのブランチでPRを出すと、該当するIssueと自動で紐づきます。

画面キャプチャ
Issueキーを含んだブランチ名をコピーするボタン
画面キャプチャ
コピーされたブランチ名

さらに、PRの状態(Open / Merged / Closed)に応じて、LinearのIssueステータスも自動的に更新されるような設定も可能です。

画面キャプチャ
PRとIssueステータスの連動オプション。PRの状態とステータスの対応関係まで設定可能

手動で手間をかけてIssueのステータスを変更する必要がなくなるため、開発に集中しながらIssueの進捗も最新状態に保つことが可能になります。

Slack連携

LinearのSlack連携は充実していて、痒いところに手が届きます。
その中でも特に社内で重宝している機能を紹介します。

Slackメッセージから直接Issueを作成

Slack上でやりとりをしている中で改善アイデアやバグ報告などが出てきたときに、そのメッセージから「Create new issue」をクリックするだけで、LinearのIssueとして簡単に登録できます。

画面キャプチャ
Slackの右クリックメニューからすぐにIssue登録が可能です

プロジェクトや担当者、優先度なども「Create new issue」を押した後に表示されるフォームで設定できるため、Issueの情報入力もスムーズに行えます。

画面キャプチャ
Issueの情報を入力するフォーム

日々の会話の中で手軽にIssueの追加ができることで、「あとで登録しようと思って忘れた」というようなことが減り、タスクの漏れが少なくなりました。

Slackワークフローとの連携

開発チームが業務を進める上で、他チームとのタスクのやり取りも頻繁に発生します。

私たちのチームでは、Slackのワークフロー機能を使って他チームからの依頼をLinearに登録する仕組みを構築しています。

具体的には、Slack上にタスクの依頼専用チャンネルを用意しています。他チームのメンバーがそこでSlackワークフローを起動してフォームに必要事項を入力すると、依頼内容がチャンネルに送信され、連動してLinearにも自動でIssueが作成されるという流れです。

画面キャプチャ
Slackワークフローの流れ

また、SlackのスレッドとLinearのコメントを同期する機能も活用することで、タスクに関するやり取りは他チームが使い慣れているSlack上で行いながら、その内容が自動的にLinearに記録される運用を実現しています。

Slackワークフローとの連携によって、他チームとの依頼の管理ややり取りがスムーズになり、
「この依頼、今どうなってたっけ?」
「やり取りって、どのチャンネルでしてたんだっけ?」

といった、タスク管理でよくある悩みがぐっと減りました。

Ask機能について

Linearには「Ask」という、Slack上での依頼を管理する専用機能も用意されています。

テックドクターではAskが使えるプランを利用していないため現在は使用していませんが、Askを使えば、Slackワークフローを用いた運用と同様のことがより手軽に実現できます。

Linear Asks – Linear

おわりに

プロジェクト管理ツールとしてLinearを導入して以降、日々のプロジェクト管理が驚くほど楽になりました。

繰り返しになりますが、Linearは単なるプロジェクト管理ツールではなく、「プロジェクト管理の手間を最小化し、顧客に価値を提供するための開発に集中できるツール」だと実感しています。

「もっと使いやすくてシンプルなプロジェクト管理ツールが欲しい」
「プロジェクト管理作業の時間を減らして開発に集中したい」
そんな方は、ぜひ一度Linearを試してみてはいかがでしょうか。

プロジェクト管理ツールで悩んでいる方にこの記事が少しでも参考になれば嬉しいです。

Python大規模開発の鍵!?:最新の型ヒントで実現する型安全なコード

はじめに

はじめまして、テックドクターでエンジニアリングマネージャをしている星野です。

弊社ではPythonを活用することが多く、型ヒントを積極的に導入し、型安全なコードの実現に努めています。

Pythonの型ヒントはPython 3.5(2015年9月リリース)から導入されましたが、その後も継続的に機能追加が行われ、使いやすく進化しています。

本記事では、型ヒントの基本的な説明に加え、最新バージョンでの改善点を紹介します。

型ヒントとは

Pythonは動的型付け言語のため、変数の型を指定する必要がありません。

そのためコードの記述が簡潔になりますが、一方で実行するまで型エラーが検出できず、予期しないバグの原因となることがあります。

大規模開発では、実行するまで動作が保証されないことが大きなリスクとなるため、Python 3.5 から型ヒントが導入されました。型ヒントを活用することで、実行前に型の整合性をチェックできます。

  • 型ヒントを使わない場合の書き方
def join_comma(lst):
  return ",".join(lst)

  • 型ヒントを使う場合の書き方
def join_comma(lst: list[str]) -> str:
  return ",".join(lst)

このように、型ヒントを追加することで、引数や戻り値の型を明示でき、コードの可読性と安全性が向上します。

ただし、Python自体は型ヒントを実行時に考慮しないため、静的型チェックツール(mypy など)を活用する必要があります。

Pythonでの静的型チェックツール

Pythonには標準の型チェック機能がないため、外部ツールを使用して型チェックを行います。

弊社ではGitHub Actions に静的型チェックツールを組み込み、CI/CDの一環として実行することで必ずチェックできるようにしています。

静的型チェックツールとしては以下があります。

  • mypy : 静的型チェックツールのリファレンス実装。歴史が長く、利用者が多い。
  • pyrightMicrosoft製。高速な型チェックが特徴。VS Code拡張機能 Pylance にも組み込まれている。
  • pyre:Meta製。OCaml で実装され、パフォーマンスを重視。
  • pytypeGoogle製。型ヒントがなくても型を推論可能。

弊社では mypy を主に使用していますが、高速な pyright も試してみたいと考えています。

型ヒントの進化

ここからは、Python3.9以降で加えられた改良の中からいくつかピックアップして紹介していきます。

標準コレクション型の記法変更(Python3.9〜)

PEP 585 – Type Hinting Generics In Standard Collections によりtyping.List の代わりに list を直接型ヒントとして使用できるようになりました。

  • Python3.8以前の書き方
from typing import List

def join_comma(lst: List[str]) -> str:
    return ",".join(lst)

  • Python3.9以降の書き方
def join_comma(lst: list[str]) -> str:
    return ",".join(lst)

typing.List は非推奨となったため、新しい記法を使用しましょう。
typing.Listなどの標準コレクションのエイリアスは今後利用頻度が下がるにつれて削除される可能性があります。

Unionの記法(Python3.10〜)

PEP 604 – Allow writing union types as X | Y により、Union の代わりに |(パイプ演算子)を使用できるようになりました。

この導入によって、Noneを取るかもしれない型の表現としては以下の3パターンになっています。

  • typing.Union[int,None]
  • typing.Optional[int]
  • int | None

  • typing.Unionを利用した書き方
from typing import Union

def join_comma(lst: Union[list[str],None]) -> Union[str,None]:
  return ",".join(lst) if lst is not None else None

  • typing.Optionalを利用した書き方
from typing import Optional

def join_comma(lst: Optional[list[str]]) -> Optional[str]:
    return ",".join(lst) if lst is not None else None

  • 「|」を利用した書き方
def join_comma(lst: list[str] | None) -> str | None:
    return ",".join(lst) if lst is not None else None

どの表現が推奨というのはないのですが、チーム内ではどれかに統一されているほうが良いと思います。
個人的には、「|」がシンプルかつ、分かりやすく表現されていると思います。

type文による型エイリアス(Python3.12〜)

PEP 695 – Type Parameter Syntax で型エイリアスを type 文で定義できるようになりました。

エイリアスは、最初は変数に型を代入することで実現していました。
ただし見た目が代入文と同じであり紛らわしかったことから、その後、Python3.10でtyping.TypeAlias が導入され、明示的に型として表すことができるようになりました。
Python3.12で、type文が導入されて、代入ではなく、構文として利用することができるようになりました。

typing.TypeAliasは非推奨となったので、今後はtype文を利用していきましょう。

  • Python3.10以前
from typing import Literal

# 代入で型を実現
Environment = Literal["development", "staging", "production", "test", "local"]

env:Environment = "development"

  • Python3.10 - 3.11
from typing import TypeAlias, Literal

# TypeAliasを使うことで、型であることを明示
Environment: TypeAlias = Literal["development", "staging", "production", "test", "local"]

env:Environment = "development"

  • Python3.12以降
from typing import Literal

# type文で型を定義することが可能に
type Environment = Literal["development", "staging", "production", "test", "local"]

env:Environment = "development"


TypedDictについて

TypedDictはdictに対して、より細かい型を指定することができます。

例えば、TypedDictを使わない場合、名前と年齢を表すようなデータを定義しようとすると以下のようになります。

person1: dict[str, str|int] = { "name": "Yamada", "age": 28 }


名前と年齢とで型が違うため、値に文字列か数値を取るdict型となります。
そのため、nameというキーに対して数値を設定しても型チェックとしてはエラーは出ません。

person1: dict[str, str|int] = { "name": "Yamada", "age": 28 }
person1["name"] = 1 # mypyなどの型チェックでもエラーは出ない


ですが、TypedDictを使うと、以下のように各キーに対して型を定義することができます。

from typing import TypedDict

class Person(TypedDict):
    name: str
    age: int

person1: Person = {"name": "Yamada", "age": 28}
person1["name"] = 1  # mypyでエラーになる


上記コードでmypyを実行すると以下のエラーが出てくれます。

 error: Value of "name" has incompatible type "int"; expected "str"  [typeddict-item]


NotRequiredについて(Python3.11〜)

PEP 655 – Marking individual TypedDict items as required or potentially-missingにより、TypedDictのキーを任意項目として定義できるようになりました。

例えば、以下のコードはgender キーが省略されているので、mypyでエラーが発生してしまいます。

from typing import TypedDict, Literal

class Person(TypedDict):
    name: str
    age: int
    gender: Literal["male", "female", "other"]|None

person1: Person = {"name": "Yamada", "age": 28, "gender": None } # genderキーがあるのでOK
person2: Person = {"name": "Yamada", "age": 28} # genderキーが無いのでエラー


NotRequiredを利用すると利用すると、キーが省略可能になります。

from typing import TypedDict, NotRequired, Literal

class Person(TypedDict):
    name: str
    age: int
    gender: NotRequired[Literal["male", "female", "other"]]

person1: Person = {"name": "Yamada", "age": 28} # genderキーが無くてもOK


また、Required も導入されており、必須項目が少ない場合は total=False と組み合わせると分かりやすくなります。

totalをFalseにすると全てのキーが必須ではなくなるので、Requiredで必須項目を指定する形になります。

from typing import TypedDict, Required, Literal

class Person(TypedDict, total=False):
    name:Required[str]
    age:Required[int]
    gender: Literal["male", "female", "other"]

person1: Person = {"name": "Yamada", "age": 28} # genderキーが無くてもOK
person2: Person = {"name": "Yamada", "gender": "male"} # ageキーが無いのでエラー


Unpackと併用して**kwargs引数に型を追加(Python3.12〜)

PEP 692 – Using TypedDict for more precise **kwargs typingにて、TypedDictとUnpackを利用して**kwargs引数に対してより厳密な型指定が可能になりました。
今までは単一の型しか指定できず、さらに仕様にない引数を指定してもエラーになりませんでした。

  • Unpackを使わない場合
from typing import TypedDict, NotRequired, Literal, Unpack

def print_person(**kwargs: str|int) -> None:
    print(kwargs)
    return

print_person(name="Yamada", age=28)  # OK
print_person(name=1, age=28)         # nameにint入ってしまうがエラーは出ない
print_person(name="Yamada", aeg=28)  # ageを誤字してしまっているがエラーは出ない

  • Unpackを使う場合
from typing import TypedDict, NotRequired, Literal, Unpack

class Person(TypedDict):
    name: str
    age: int

def print_person(**kwargs: Unpack[Person]) -> None:
    print(kwargs)
    return


print_person(name="Yamada", age=28)  # OK
print_person(name=1, age=28)         # nameにintが指定されているのでエラー
print_person(name="Yamada", aeg=28)  # ageを誤字してしまっているのでエラー


読み取り専用のアイテム定義:ReadOnly(Python3.13〜)

PEP 705 – TypedDict: Read-only itemsにより、一部のキーを読み取り専用に設定できるようになりました。

一部のキーだけ値の変更をしたくない場合に利用します。

from typing import TypedDict, ReadOnly, NotRequired, Literal

class Person(TypedDict):
    name:ReadOnly[str]
    age: int
    gender: NotRequired[Literal["male", "female", "other"]]

person1: Person = {"name": "Yamada", "age": 28, "gender": "male"}
person1["age"] = 30  # OK
person1["name"] = "Tanaka"  # nameはReadOnlyなのでエラー

まとめ

バージョンアップする毎に型ヒントが色々と改良されており、自身でもきちんと把握したいと思い今回の記事を執筆しました。

TypedDictについては、今回執筆するにあたって初めて知った機能です。
既存のコードで既にdictを使っている部分に型を導入する際、既存のコードへの変更なしに型安全にすることができるので、試してみたいです。

今回は、Generic関係については触れられていないのでどこかでまとめたいと思います。