第22回日本うつ病学会にて、抑うつ気分の予測に関するポスター発表を行いました

こんにちは、データサイエンス部の深見です。

テックドクターのデータサイエンスチームでは、定期的に学会への参加や登壇を行なっています。今回は7/11-12に浜松町で開催された第22回日本うつ病学会総会にてポスター発表を行なってきましたので、その内容を紹介いたします。

私自身はちょうど一年前の日本睡眠学会での登壇以来、久しぶりの学会参加でした。またポスター発表となると本当に久しぶりで、最後は学生時代だったかもしれません。

私以外にもテックドクター社としては、これまでにも日本リウマチ学会や国際医薬経済・アウトカム研究会(ISPOR)など国内外の様々な学会に参加して研究の成果を報告しています。

www.technology-doctor.com

今回の発表内容は、いわゆるケーススタディという形式で、一人の被験者の長期に渡るウェアラブルバイスのデータを解析したものです。

ウェアラブルバイスを使うと、これまでは病院に来院したタイミングでしか分からなかった被験者の状態が、連続的にかつ細かな粒度で把握できます。このことが医療の質を上げるのに役立つと期待されています。

特にスマートフォンウェアラブルバイスから得られる生体データをもとにした指標「デジタルバイオマーカー」は、疾患の有無や病状の変化を客観的に評価することができる可能性を秘めており、その開発は国内でも注目が高まっています。

※デジタルバイオマーカーの詳細については弊社の下記サイトを参照してください。
デジタルバイオマーカー | TechDoctor

研究概要

演題:
双極性障害患者におけるウェアラブルバイスを用いたデジタルバイオマーカーの探索」

被験者:
2010年に診断を受けた双極性障害患者1名(40代・男性)

目的:
双極性障害の症状(特に抑うつ気分)に関連するデジタルバイオマーカーの発見。

対象データ

収集したデータは、ウェアラブルバイスFitbit Charge 6を使った生体データ(測定値)と、eMoodsというアプリを使った主観データ(自己申告の症状記録)です。それぞれどんな項目が対象かは、下記の表を見てください。

収集データの一覧表

期間としては2024年2月から11月まで9ヶ月間のデータを解析対象としました。こちらは現在も収集が続いており、今後も継続して解析を実施する予定です。

解析方法

主観データの中でも、特に「抑うつ気分」に着目、この指標と連動する生体データがないか調べました。

具体的には、

心拍数データをもとに算出した心拍変動指標
例)
・睡眠中の平均心拍数
・自律神経の活動を評価する複数の心拍変動

睡眠指標
例)
・ 睡眠時間
・各睡眠ステージ(覚醒/レム睡眠/浅い睡眠/深い睡眠)の時間や比率

などを使用しました。

これらの指標を1日ごとに算出、大きく変動した値(全期間の平均と標準偏差から、平均値±標準偏差以上、離れているもの)を異常値として抽出しました。

解析結果

副交感神経の活動を評価するRMSSD(Root Mean Square of Successive Differences)という心拍変動指標があります。一般的に、RMSSDの数値が大きくなると副交感神経が優位になる=リラックスした状態、逆に値が小さくなるとリラックスできていない状態と考えることができます。

今回の研究により、睡眠中のRMSSDの値に抑うつ気分との関連が見られることがわかりました。
実際のデータを見てみましょう。

睡眠中のRMSSDの値と抑うつ気分との関連グラフ

特に期間の後半、抑うつ気分の高まり(図中、青棒グラフが2点以上)とともにRMSSDのトレンド成分(図中、赤線グラフ)が下降していました。

また期間中にRMSSDが平均より大きく低下(1.5SD以上)した日が14日ありましたが、そのうち12日にその後1週間以内に抑うつ気分スコアの上昇が確認されました。

これらの結果は、睡眠中の副交感神経活動の低下を見ることでその後の抑うつ気分を予測できる可能性を示しています。もっと研究を進めることで、抑うつ気分を推定するバイオマーカーとして有用と判明するかもしれません。

今回の発表では一人の被験者に対して抑うつ気分との関連を調査しましたが、抑うつ気分以外の主観指標と生体データの関連の解析や、大人数を対象にした検証も実施しています。今後さらに内容を充実させた上で論文を投稿する予定です。

発表を通しての印象

ポスター発表では演題が60以上あり、演題番号が奇数のものが初日に、偶数のものが2日目にそれぞれ30分発表を行う形式でした。

私の発表は2日目の土曜日の14時からと比較的足を運びやすい時間帯だったこともあってか、時間中ひっきりなしに質問があり、5名程度の方に説明をすることができました。それ以外にも足を止めてくださる方、遠目に見ていただける方が何名かいらっしゃいましたのでそれなりに興味を持っていただけたのではないかと思います。
※会場内撮影禁止でしたので残念ながら写真はありません…

またポスター発表の他にも、父親の産後うつに関するセッションや、精神疾患の治療において薬を使った場合と精神療法を用いた場合の比較に関するセッションに参加し、最新の研究に触れることができました。こういった知見を日々の研究に活かしていきたいと思います。

まとめ

いかがでしたでしょうか。テックドクターは、研究成果を今回のような学会発表や論文投稿という形で発表することで、医学の発展への貢献を目指しています。また対外的な発表以外にも特許という形で成果を残すこともあります。今後も学会発表や論文投稿した際には随時発信していきたいと思います。

似顔絵
書いた人:深見

クライアントとサーバーで、APIの型・バリデーションルールを一元化する

こんにちは。株式会社 TechDoctor でソフトウェアエンジニアをしている大瀧です。

突然ですが、アプリケーション開発で、クライアントとサーバーの API の型やバリデーションルールが食い違い、予期せぬバグに繋がった経験はないでしょうか。

このような課題の解決策として、本記事では、OpenAPI スキーマをもとに、API の型とバリデーションルールを一元管理する方法を紹介します。

ここで紹介するアプローチは、 OpenAPI スキーマを作成できる限り特定の言語やフレームワークに依存しませんが、今回は具体例として FastAPI、Pydantic、Zod、openapi-zod-client を使った方法を解説します。

なお本記事のサンプルコードは、以下のリポジトリで確認できます。(Kiro と Claude Code が作ってくれました)
GitHub - shutootaki/sync-schema-demo

1. 背景と課題

課題 ①:API の型の二重管理

API のインターフェースの型が、バックエンドとフロントエンドでそれぞれ定義されていると、二重管理になってしまいます。

この状態では、仕様変更のたびに両方のコードを手動で同期させる必要が生まれます。

その際に片方で修正漏れが起きると、データの不整合や実行時エラーに直結します。結果として、ランタイムエラーが発生したり、「このフィールドは null 許容でしたっけ?」といった本来不要なコミュニケーションが頻発してしまいます。

課題 ②:バリデーションロジックの分散

優れた UX のためにはクライアント側での事前バリデーションが必要です。そしてデータの整合性とセキュリティのためにはサーバー側でのバリデーションも不可欠です。

しかし、これらのバリデーションルールをそれぞれ別のリポジトリで管理していると、いつの間にか仕様が乖離しがちです。

結果として、課題 ① と同様にエラーや不要なコミュニケーションコストが発生してしまいます。

2. 解決アプローチ

この課題を解決するために、弊社では以下のアプローチを採用しました。

バックエンドを信頼できる唯一の情報源(SSoT)にする

バックエンド(FastAPI + Pydantic)を、型とバリデーションルールにおける信頼できる唯一の情報源と位置づけます。

そのために、コーディング時はPydantic でリクエスト/レスポンスの型とバリデーションルールを定義します。
あとは自動的に同期が行われる仕組みにします。

コード生成による型の自動同期

同期のしくみはこうです。

概念図

Pydanticでのスキーマ定義をもとに、FastAPI が OpenAPI スキーマを自動生成します。
フロントエンド側では、openapi-zod-client を使って、OpenAPI ドキュメントをもとにZod スキーマを自動生成します。

この仕組みにより、バックエンドでスキーマを変更してもコマンド 1 つでフロントエンドの型を同期できるため、APIの型とフロントエンド側の実装との不整合を簡単に検知でき、バリデーションロジックの二重実装も不要になります。

3. 具体的な実装方法

基本的なユーザー作成 API を例に、具体的な実装方法を解説します。

① Pydantic モデルの定義と FastAPI によるドキュメント生成

バックエンドでは、Pydantic を使いリクエストとレスポンスのスキーマを定義します。

基本的なスキーマ定義
from pydantic import BaseModel, Field, EmailStr
from typing import Optional
from enum import Enum

class UserRole(str, Enum):
    USER = "user"
    ADMIN = "admin"

class UserCreateRequest(BaseModel):
    name: str = Field(..., min_length=1, max_length=100)
    email: EmailStr
    age: int = Field(..., ge=18, le=120)
    role: UserRole = UserRole.USER

class UserResponse(BaseModel):
    id: int
    name: str
    email: str
    age: int
    role: UserRole
FastAPI ルーターでのスキーマの使用

定義した Pydantic スキーマを、FastAPI のエンドポイントで利用します。

from fastapi import APIRouter, HTTPException
from app.models.user import UserCreateRequest, UserResponse
import random

router = APIRouter(tags=["Users"])

users_db: dict = {}

@router.post("/users", response_model=UserResponse)
async def create_user(user_data: UserCreateRequest) -> UserResponse:
    new_user = {
        "id": generate_user_id(),
        "name": user_data.name,
        "email": user_data.email,
        "age": user_data.age,
        "role": user_data.role,
    }

    users_db[new_user["id"]] = new_user
    return UserResponse.model_validate(new_user)
FastAPI アプリケーションの設定

main.py で、OpenAPI ドキュメント生成の設定を行います。

from fastapi import FastAPI
from app.routers import users

app = FastAPI(
    title="User API",
    version="1.0.0",
    description="シンプルなユーザー管理API"
)

app.include_router(users.router, prefix="/api/v1")
生成される OpenAPI スキーマ

上記の Pydantic モデルと FastAPI の設定から、次のような OpenAPI スキーマが自動生成されます。(一部抜粋)

{
  "openapi": "3.1.0",
  "info": {
    "title": "User API",
    "version": "1.0.0"
  },
  "components": {
    "schemas": {
      "UserCreateRequest": {
        "properties": {
          "name": {
            "type": "string",
            "maxLength": 100,
            "minLength": 1,
            "title": "Name"
          },
          "email": {
            "type": "string",
            "format": "email",
            "title": "Email"
          },
          "age": {
            "type": "integer",
            "maximum": 120,
            "minimum": 18,
            "title": "Age"
          },
          "role": {
            "$ref": "#/components/schemas/UserRole",
            "default": "user"
          }
        },
        "title": "UserCreateRequest",
        "description": "ユーザー作成リクエストのスキーマ"
      },
      "UserRole": {
        "type": "string",
        "enum": ["user", "admin"],
        "title": "UserRole"
      }
    }
  }
}

画面キャプチャ

このスキーマには、minLength=1ge=18 といったバリデーションルール、UserRoleEnum 定義、必須フィールドの指定、整数・文字列・メール形式といった詳細な型情報が含まれています。

openapi-zod-clientによるスキーマファイルの生成

フロントエンド側では、FastAPI が生成した OpenAPI スキーマをもとに、openapi-zod-clientを使って Zod スキーマを自動生成します。

openapi-zod-clientのインストールと設定

まず、フロントエンドプロジェクトに必要なパッケージをインストールします。

pnpm install -D openapi-zod-client

次に、プロジェクトのルートに設定ファイルを作成します。

// schema-format.hbs

// This file is automatically generated. Do not change it manually.
import { z } from "zod";

{{#each schemas}}
export const {{@key}} = {{{this}}};
export type {{@key}} = z.infer<typeof {{@key}}>;
{{/each}}

export const schemas = {
{{#each schemas}}
	{{@key}},
{{/each}}
};

上記はシンプルな設定例です。詳細は公式ドキュメントを参照してください。
GitHub - astahmer/openapi-zod-client: Generate a zodios (typescript http client with zod validation) from an OpenAPI spec (json/yaml)


package.jsonへのスクリプト追加

開発ワークフローを効率化するため、package.jsonスクリプトを追加します。

{
  "scripts": {
    "codegen": "openapi-zod-client -o ./src/lib/api/schema/api_schemas.ts -t ./schema-format.hbs ${OPEN_API_JSON:-<http://localhost:8000/openapi.json>}"
  }
}

バックエンドサーバーが起動している状態で以下のコマンドを実行すると、Zod スキーマが自動で生成されます。

pnpm run codegen

実際には以下のような Zod スキーマが自動で生成されます。(一部抜粋)
OpenAPI スキーマをもとに UserCreateRequest の型とバリデーションルールが、Zod のスキーマに変換されています。

// src/lib/api/schema/api_schemas.ts
// This file is automatically generated. Do not change it manually.
import { z } from "zod";

export const UserRole = z.enum(["user", "admin"]);
export type UserRole = z.infer<typeof UserRole>;
export const UserCreateRequest = z
  .object({
    name: z.string().min(1).max(100),
    email: z.string().email(),
    age: z.number().int().gte(18).lte(120),
    role: UserRole.optional(),
  })
  .passthrough();
export type UserCreateRequest = z.infer<typeof UserCreateRequest>;
React Hook Form との連携

生成された Zod スキーマは、React Hook Form のようなフォームの状態管理をするライブラリと簡単に連携できます。

以下のように、src/lib/api/schema/api_schemas.ts に出力された Zod スキーマを import して、useForm と zodResolver にセットするだけで、先程 Pydantic で定義した型とバリデーションルールを再利用することができます。

import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { UserCreateRequest } from "@/lib/api/schema/api_schemas";

export const UserForm = ({ onSubmit, isLoading }) => {
  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm<UserCreateRequest>({
    resolver: zodResolver(UserCreateRequest),
    defaultValues: { role: "user" },
  });

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register("name")} placeholder="名前" />
      {errors.name && <p>{errors.name.message}</p>}

      <input {...register("email")} type="email" placeholder="メール" />
      {errors.email && <p>{errors.email.message}</p>}

      <input {...register("age", { valueAsNumber: true })} type="number" />
      {errors.age && <p>{errors.age.message}</p>}

      <button type="submit" disabled={isLoading}>
        {isLoading ? "作成中..." : "ユーザーを作成"}
      </button>
    </form>
  );
};

実際にサンプルコードのフロントエンドサーバーを起動して、フォームに不正な値を設定すると、以下のように期待通りバリデーションを実行してくれていることがわかります。

画面キャプチャ

4. まとめ

本記事では、FastAPI、Pydantic、Zod、openapi-zod-client を組み合わせ、API の型とバリデーションルールを一元管理する方法を紹介しました。弊社では、この取り組みによって以下のメリットを得ることができました。

  • 型の不一致によるバグを予防
    • バックエンドで定義した OpenAPI スキーマからフロントエンドの型を生成するため、型の不一致によるバグを大幅に減少させることができました。
  • バリデーションロジックを再利用
    • Zod へ変換されたスキーマを React Hook Form などに直接適用できるため、同一のルールを重複して実装する必要がなくなりました。
  • 変更への追従が容易
    • バックエンドのスキーマモデルを修正した後に、コード生成コマンドを実行するだけで API の型定義・バリデーションロジックが更新されるので、開発者の負担を減らすことができました。

API の型・バリデーションロジックの管理に課題感を持っている方がいましたら、ぜひ導入を検討してみてください!



書いた人:大瀧

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よりもバラついている、つまり「睡眠リズムがバラバラ」ということを示す結果となっています。

おわりに

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

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


似顔絵
書いた人:坂本