PyCon JP 2024参加レポート&良かったセッション紹介

はじめまして、バックエンドエンジニアの伊藤です。

9月に開催されたPython のカンファレンスイベント「PyCon」に、テックドクターのバックエンドエンジニア3人で参加してきました。

今回はその体験記として、イベントの様子や気になった発表などをレポートします。

PyConとは

PyConについて、公式サイトではこのように説明されています。

PyCon JP は、Python ユーザが集まり、PythonPython を使ったソフトウェアについて情報交換、交流をするためのカンファレンスです。 PyCon JP の開催を通じて、Python の使い手が一堂に集まり、Python にまつわる様々な分野の知識や情報を交換し、新たな友達やコミュニティとのつながり、仕事やビジネスチャンスを増やせる場所とすることが目標です。

PyCon JP 2024 より引用

ひとことで言えば、Pythonにまつわる情報を共有することがメインのイベントです。具体的には大きく2つの要素で構成されます。セッションとスポンサーブースです。

セッション

Pythonユーザーによるスライド発表です。Pythonにまつわるノウハウや活用事例、課題解決や関連ツールの紹介など、多岐にわたる発表があります。

1つのセッションは時間にして30分ほど。登壇者はセッションは4つの会場で並行して行われるため、効率的に情報収集するためには事前にタイムテーブルを見ておき、会場を移動しながら興味のある発表を聞くのがおすすめです。発表資料の一部がこちらに掲載されていますので、今回参加できなかった方もどんな内容か知ることができます。

スポンサーブース

スポンサーブースは、PyConに出資したスポンサー企業が自分たちの魅力を紹介するためのブースです。こちらはセッションとは違い常設ブースとなっており、PyCon参加者は誰でも立ち入ることができます。ノベルティを配っている企業も多く、お祭りの屋台のような形で楽しめました。

ブースの写真
Forkwellさんの「恐怖の鳥釣り」ブース。Pythonにちなんで、ヘビの釣り竿でアヒルちゃんを釣るというゲームをやっていました。
ブースの写真
KRAKENさんのブース。アンケート回答でかわいいクラーケンの指人形がもらえました。
ブースの写真
GlobalWayさんのブース。トートバッグ等を配布されていました。
ブースの写真
Findyさんのブース。モバイルバッテリーやお守りが当たるガチャくじを実施されていました。
ノベルティの写真
たくさんのノベルティをいただきました!

PyConに参加した目的

今回は個人的な参加ではなく、テックドクターの社員として、2つの目的でPyConに参加しました。

ひとつはほかの参加者の皆さんと同じく、Pythonについての理解を深めることです。

もうひとつは、スポンサー協賛の検討です。テックドクターでは日頃からお世話になっているPythonのコミュニティ発展に貢献するため、PyConへのスポンサー協賛を検討しています。PyCon自体の雰囲気を知ることと、スポンサーになるとどのようなことが行えるのかなど検討材料の収集を行いました。

良かったセッション

テックドクターからは私(伊藤)以外に、星野、魚木が参加しました。二人とも私と同じバックエンドエンジニアですが、それぞれPythonの経験年数は違います。またPyConは3人とも初参加でした。ここでは今回は私と星野が選んだ、良かったセッションをご紹介します。

伊藤(Python経験 7年)の印象に残ったセッション

「MLOps in Mercari Group’s Trust and Safety ML Team」

発表者等: Calvin Janitra Halim, Ayato Toyokuni(株式会社メルカリ)

メルカリさんのMLOpsのアーキテクチャについての発表です。
最近テックドクターでも機械学習モデルの本番利用に向けたシステムの構築を行っており、試行錯誤しながら進めている段階のため、非常に参考になるセッションでした。

アーキテクチャ図がふんだんに用いられていることで、複雑な構成も視覚的にも論理構造的にもわかりやすく、参考にしやすいと感じました。また英語での発表だったものの、図のおかげで理解しやすかったです。

弊社のアーキテクチャはよりシンプルな作りでも問題ないはずですが、先人の事例と比較することで方向性が大きく間違っていないことを確認できたのは、大きな収穫でした。(伊藤)

星野(Python経験 3年、Pythonでの大規模開発経験なし)の印象に残ったセッション

「あなたのアプリケーションをレガシーコードにしないための実践Pytest入門」

発表者:fujine (みずほリサーチ&テクノロジーズ株式会社 先端技術研究部)

Pythonに限らずテスト全般の内容が幅広く含まれた発表でした。テストをあまり書いたことがない人でもわかりやすかったのではないかと思います。

導入部の切り口も面白く、「レガシーコードとは」から始まる冒頭にまず興味を惹かれました。その後、データベースや時刻処理などのありがちなテストパターンにどう対処するかについて詳しく解説され、実践的なケースに対する理解が深まりました。

また、Pytestの小技も紹介され、今後テストを作成する際に参考になりそうな内容でした。Pytest初心者の私にとっては、今後使うであろうトピックスがまとまっており学びが多かったです。(星野)

全体の感想

個人としてもテックドクターとしても初のPyCon JPへの参加でしたが、まずは参加すること自体が楽しいイベントだと思いました。星野からも「久しぶりのテックカンファレンスで純粋に楽しかった」というコメントがありました。

セッションはここでは2つだけご紹介しましたが、それ以外にも良いものがたくさんありました。全体の傾向としては、Python初級者から中級者向けの発表が多かったように思います。(あくまで推測ですが)Pythonの利用用途が広範囲になっているので、間口を広くとるために初学者向けのセッションを多くしたのかもしれません。

カンファレンスイベントに敷居の高さを感じる人もいるかもしれませんが、少なくともPyConは、Pythonを使いはじめたばかりの人が気軽に参加しても得られるものが多くありそうです。

上級者が参加する場合は、初心者向けのセッションに当たって物足りない思いをしないように、あらかじめタイムテーブルを見て自身のレベルにあったセッションを選んでおくとより効率的な情報収集を行えそうです。

スポンサー出展について

将来的な出展を意識してスポンサーブースを回ってみると、単にノベルティをばらまくのではなく、ブースの方々が来場者を飽きさせないように配布方法やタイミングなどいろいろと工夫されていることがわかり、面白かったです。

また、何を目的にスポンサーをするのかで、どこに重きを置くのかが変わってきそうだと感じました。
例えば会社の知名度を上げたいという目的であれば、豪華なスポンサーブースを作るよりも、セッションに何人か登壇する方が結果として印象に残りやすいように思います。

まとめ

来年のPyCon JPは広島での開催だそうです。今回の体験を踏まえて、実際にスポンサーをするのか検討していきたいと思います。

運営事務局の皆さん、発表者・出展者の皆さん、楽しく役に立つカンファレンスをありがとうございました。

似顔絵
書いた人:伊藤

「意味のある外れ値」のデータ解析 〜極端に高い心拍数を定量的に評価する〜

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

みなさん、データ処理における「外れ値」と聞くと、真っ先に除外すべきものというイメージをお持ちではないでしょうか。実は、必ずしもそうとはいえない場合もあるのです。

TechBlog第八回では、ウェアラブルデータ(※)解析の面白さを知っていただくために、この「外れ値」をめぐるひとつの事例をご紹介したいと思います。

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

ウェアラブルデータの分析手法

ウェアラブルデータの分析では、目的に応じてさまざまな手法を使います。

代表的なものには、2つの群の差を取り扱う手法(t検定、分散分析等)や、データとデータの関連性を捉える手法(相関分析、一般化線形モデル等)、反復測定を考慮した手法(マルチレベル分析等)、分類や予測に適した手法(機械学習、深層学習等)、グループ分けに適した手法(クラスター分析等)などがあります。

これらの手法を駆使してデータの切り口を工夫したり、的確な分析手法を選択したり、ときには分析手法自体をオーダーメイドすることで、データからいろいろな情報を抽出することができます。

ウェアラブルデータは非常に豊かな情報源です。同じデータに対しても、さまざまなアイデアと手法を駆使することで、多様な研究に使える・発展する可能性を秘めています。言いかえれば、解析者が自身のアイデアでデータの持つ可能性を広げられる点こそが解析の面白さでもあります。

未知なる可能性を秘めたウェアラブルデータの解析。なかでも今回注目したいのが、先ほども触れた「外れ値」です。

眠っている男性のイメージ

極端に高い心拍数の捉え方

研究やデータ解析において、「外れ値」は除外対象として捉えられるのが一般的です。外れ値が、たとえばセンサーの誤作動といったような「適切に測定されなかったことに由来するノイズ」である場合、それを含めて分析してしまうと、結論に誤りが生じてしまう可能性があるためです。

しかし、一見すると除外対象のように思える「外れ値」も、ノイズとは異なる重要な意味を持っている場合があります。それを本稿では「意味のある外れ値」と表現しています。

たとえば、解析対象のデータが「前方に道路を横切る歩行者が現れてから、運転者がブレーキを踏むまでの時間」であったとします。このようなケースでは、ブレーキを踏むまでの平均時間よりも、極端な踏み遅れ(=意味のある外れ値)が重要な意味を持つことになります。事故につながるリスクが格段に高い現象だからです。意味のある外れ値がどのような条件の時に生じやすいのか。運転者が強い眠気を感じていたのか、歩行者を視認しにくい特別な交通状況があったのか……がわかれば、対策を立てることができます。

こう考えると、意味のある外れ値を除外せず、研究することの重要性がよくわかると思います。

同じくして、「極端に高い心拍数」にも重要な意味があると考えられます(もちろん、測定ミスの場合を除いて)。その背景には、急激な運動、動機や息切れの発生、心疾患などの病気や、手術後で弱った身体への負担、強度の緊張の発生など、さまざまな理由が想像できます。極端に高い心拍数の発生を定量的に評価することができれば、さまざまな研究への応用が期待できそうです。

では、実際に「定量的に評価する」方法にはどのようなものがあるでしょうか?

指数−ガウス分布

外れ値のような極端に大きな値を除外せず、解析的に考慮するための手法の一つに、「指数−ガウス分布(執筆者訳。一般的にはex-Gaussian Distributionや Exponentially Modified Gaussian Distribution等と呼ばれる)」を用いた分析があります。先ほど例に挙げた「ブレーキを踏むまでの時間」など、反応にかかる所要時間の分析に用いられることがあります。

指数−ガウス分布は、基本的なデータのばらつき部分を表現する正規分布と、外れ値に相当する成分を表現する指数分布とを合成(畳み込み積分により導出)した確率分布です。「山なりで右に裾が長い(重い)分布」という特徴的な形状を持ちます(図1)。

グラフ
図1 正規分布・指数分布・指数−ガウス分布の例 (μ=25, σ=3, τ=10の場合)

合成元となる正規分布の平均(μ)と分散(または標準偏差σ)、指数分布の期待値(τ、場合により、その逆数のλ)がパラメータであり、特に「外れ値の大きさや出現頻度に関する情報」をパラメータτで捉えることができます。データが指数−ガウス分布にしたがう(近似できる)と仮定し、パラメータ推定を実施することで、これらを定量化することができます。

睡眠中の心拍数データの分析

図2は、特定の期間に測定された、睡眠中の心拍数の度数分布です。2名分の実際の測定データ(※)を可視化しています。

※ 社内メンバーの提供データであり、お預かりした研究データではありません。

グラフ
図2 睡眠中の心拍数の度数分布

※最小値と最大値を破線、中央値を実線で示しています。

横軸は1分あたりの心拍数です。その心拍数が睡眠中の何分間、観測できたかを表すのが縦軸です。グラフからは、Bさんのみ1分あたり100以上の心拍数が観測されていることがわかります(赤矢印の箇所)。

Aさんは、睡眠中の最小心拍数が約50、最大心拍数が約80です。測定データはこの区間において「山なりで、右に裾が長い(重い)形状」で分布していることがわかります。

他方、Bさんも「山なりで、右に裾が長い(重い)形状」であることに変わりはありませんが、その裾がAさんよりも長い(重い)です。最大値も180を超えており、ときおり極端に高い心拍数が観測されていることがわかります。

「睡眠中の1分あたりの心拍数が80を超えたものは極端な値(=外れ値)」として、その割合を求めるなどの方法も考えられます。ただ、個人によって心拍数の平均値などが違うことを考えると、一律に「心拍数が80を超えたもの」のようなしきい値を決めるのではなく、個人差を考慮できる何らかの統計分析手法を適用した方がよいでしょう。

そこで今回は、AさんとBさんの2名について、それぞれのデータを用いて指数−ガウス分布のパラメータを推定します。統計解析環境R(version4.3.3)と、ハミルトニアンモンテカルロ法(NUTSアルゴリズム)を搭載したソフトウェアSTAN(version 2.32.2)を使用します(iteration = 2000, warmup = 1000, thinning = 4, chains = 4, total samples = 1000)。分析モデルの詳細は、本稿末尾をご覧ください。

分析結果

パラメータ推定の結果、全てのパラメータについて適切に推定できていることが確認できました(Rhat統計量<1.01、実効サンプルサイズ>10%、モンテカルロ標準誤差/標準偏差 < 10%)。

実際の度数分布と事後予測分布を重ね、視覚的にチェックしてみましょう(図3)。

※事後予測分布…既に観測されたデータに基づいて、将来のデータや新しい観測がどのような値をとるかを予測するために使う分布。ここでは先ほどパラメータ推定した指数−ガウス分布のこと。

グラフ
図3 視覚的事後予測チェック(Visual Posterior Predictive Checks)

※破線は測定データの最小値と最大値を示す。

図3では、実際の測定データの度数分布(ヒストグラム)と、曲線で示した事後予測分布(5つのiterationを無作為に抜粋)を重ねて表示しています。指数−ガウス分布によって、睡眠中の心拍数のばらつきかた(分布)を的確に捉えられていると判断してよさそうです。

次に、パラメータの推定結果も見ていきましょう。推定の結果、Aさんより、Bさんの方が、外れ値の頻度や大きさを表すパラメータτの値が高いことが確認されました(δ(τA - τB) = -0.508[-0.887, -0.1234])。結果を図4に示します。

グラフ
図4 指数−ガウス分布を用いた睡眠中心拍数のパラメータ推定結果

※点は事後分布の期待値(Expected A Posteriori)を示し、エラーバーは95%信用区間(Credible Interval)を示します。

技術の応用と研究への発展

このように、指数−ガウス分布を用いた解析は、「意味のある外れ値」を定量的に評価する際に有用だと考えられます。

さらにここから、さまざまな分析モデルへと発展させることもできそうです。「年齢が高くなるほど、パラメータτの値が高くなる」ことを仮定した回帰モデルや、「対照群より疾患群の方がパラメータτの値が高い」という個人と集団の階層性を仮定した分析モデル、そして、「手術直後からの3ヶ月間でパラメータτの推定値が徐々に低下した」などを取り扱う時系列モデルなど、やり方次第で多くの研究に貢献できる可能性があります。

もちろん、適用範囲は「極端に高い心拍数」だけではありません。極端に長い睡眠時間の発生や、極端に多い運動量など、さまざまな測定データに対して活用できる可能性があります。

「極端に高い」や「極端に長い」がキーワードになる現象を取り扱う際には、指数−ガウス分布の活用を検討してみてください。ウェアラブルデータを用いた詳細な研究等のご相談は、ぜひ弊社まで。

分析モデルとスクリプト(.stanファイル)

分析モデルを記載したStanスクリプト

// ExGaussian.stan

// 入力データ
data {
  int<lower=0> N;     // 対象者数(今回はN=2)
  int<lower=0> r;      // 観測したすべての心拍数の数(レコード数)
  int<lower=1> ID[r];   // 観測レコードに対応した対象者のID番号(A=1, B=2)
  vector[r] Y;          // 観測した1分ごとの心拍数の値
  int Const;            // 心拍数のスケールを調整する定数(今回はConst = 60)
}

// データの前処理
transformed data {
  // 0付近の正の実数で安定した推定をするために、スケールを秒単位に。
  vector[r] rescaled_Y;
  rescaled_Y = Y / Const;
}

// 推定パラメータ
parameters {
  // 個人ごとのパラメータ
  vector[N] mu;
  vector<lower=0>[N] sigma;
  vector<lower=0>[N] tau;
}


// モデル
model {
  // 事前分布
  mu ~ normal(0, 10);
  sigma ~ student_t(3, 0, 1);
  tau ~ gamma(0.01, 0.01);
  // 尤度関数
  for(i in 1:r) {
    // stanでは指数-ガウス分布の引数にλを使用するため、1/τを所与する
    rescaled_Y[i] ~ exp_mod_normal(mu[ID[i]], sigma[ID[i]], inv(tau[ID[i]]));
  }
}

// 事後予測分布の生成
generated quantities {
  vector[r] predY;
  for(i in 1:r) {
    // 生成した予測分布を元のスケールに戻す
    predY[i] = Const * exp_mod_normal_rng(mu[ID[i]], sigma[ID[i]], inv(tau[ID[i]]));
  }
}


似顔絵
書いた人:坂本

HealthKitを使ってAppleデバイス向け健康管理アプリ開発を始める方法

こんにちは。プロダクト開発チームでネイティブアプリの開発を担当している大嶋です。

この記事では、Appleが提供する健康データ管理のためのフレームワークであるHealthKitを活用し、健康管理アプリを開発する方法について解説します。

HealthKitを利用することで、iOSおよびwatchOSアプリがユーザーの健康やフィットネスデータを収集・管理できるようになります。具体的には、iPhoneやAppleWatchが収集したデータをアプリから読み込んだり、逆にアプリで収集したデータをHealthKit経由で蓄積することも可能です。

これから健康やフィットネスに関するアプリを開発したいと考えている方は、ぜひ本記事を参考にしてください。

Apple公式サイト「ヘルスケアとフィットネス」より引用

HealthKitの概要

HealthKitは、ユーザーの同意のもとでアクティビティ・心拍数・体重・栄養・睡眠などのデータを統合し、Appleの「ヘルスケア」アプリや「フィットネス」アプリで一元管理するためのフレームワークです。

例えば、月次の運動量を確認するとか、睡眠の質を確認するといった機能のアプリを比較的簡単に作ることができます。

データはデバイス内部のローカルストレージに保存されるため、ユーザーの健康データを安全に管理できます。

HealthKitを使用する利点としては、下記の3点が挙げられます。

  • データが統合管理できる
    「ヘルスケア」アプリで様々な情報を統合的に閲覧・管理できるため、ユーザーが自身の健康状態を把握しやすくなります。
  • プライバシー保護がしやすい
    データへのアクセスにはユーザーの明示的な許可が必要なため、データの利用や共有は厳密に管理されます。フレームワーク側でそれらの機能が実装されているので、アプリ側の設計で考慮する必要がありません。
  • サードパーティアプリと連携可能
    データを直接蓄積したアプリ以外のアプリからも、HealthKitを介してデータにアクセスすることができます。より広範囲な健康管理が可能です。

HealthKitを使ったアプリ開発の準備(Xcodeでの設定)

ここからは、実際の使用方法を紹介していきます。
HealthKitの利用の前に、Xcodeでの設定が必要です。以下の手順で設定を行います。

  1. CapabilityにHealthKitを追加
    プロジェクトのターゲット設定から「HealthKit」を有効化します。
  2. Info.plistにHealthKit関連の項目を追加
    HealthKitを使用するためには、Info.plistにHealthKitの利用目的を記載します。利用目的は、特にリリース時にはユーザーが理解できるように丁寧に記述する必要があります。

HealthKit実装サンプルの解説

次に、実際のHealthKitの実装についてもサンプルコードとともにご紹介します。実装例はGitHubに公開しているので、以下のリンクから確認してみてください。

github.com

アプリの概要

このサンプルアプリは、以下の3つの健康データを表示します。

  • 歩数:当日の合計歩数を表示します。
  • 平均心拍数:当日の平均心拍数をBPM(beats per minute)で表示します。
  • 睡眠時間:当日の睡眠時間(in bedの時間)を時間単位で表示します。
サンプルアプリの画面

また、画面にある「データを更新」ボタンを押すと、最新のデータが取得され、表示が更新されます。

アプリ開発において、考慮すべき項目がいくつかあります。

HealthKitの権限管理

概要のパートでプライバシー保護について書きましたが、HealthKitを利用する際には、ユーザーから必要なデータのアクセス権を得る必要があります。これを行うのがrequestAuthorizationメソッドです。

func requestAuthorization(completion: @escaping (Bool, Error?) -> Void) {
    let stepType = HKObjectType.quantityType(forIdentifier: .stepCount)!
    let heartRateType = HKObjectType.quantityType(forIdentifier: .heartRate)!
    let sleepType = HKObjectType.categoryType(forIdentifier: .sleepAnalysis)!
    let readTypes: Set = [stepType, heartRateType, sleepType]
    
    healthStore.requestAuthorization(toShare: nil, read: readTypes) { (success, error) in
        completion(success, error)
    }
}

requestAuthorizationメソッドでは、取得したいデータタイプをreadTypesセットで指定し、HealthKitの権限を求めます。ここでは、ユーザーの「歩数」「心拍数」「睡眠データ」にアクセスするための権限をリクエストしています。

このメソッドが実行されると、ユーザーの端末にHealthKit連携を許可するかどうかのポップアップが表示されます。

表示されるポップアップ画面の例

このメソッドが成功すると(=ユーザーが許可すると)、アプリはこれらのデータタイプに対してアクセス可能となります。

※ユーザーが許可しなかった場合は、アプリ側から再度アクセスを取ることはできません。「ヘルスケア」アプリから指標別にアクセス許可を取ることになります。

データ取得のクエリ

HealthKitでは、健康データを取得するためにクエリを使用します。ここでは、期間内の歩数データの合計を例にして、データ取得の方法を解説します。

歩数データの合計値を取得するためには、HKStatisticsQueryを使用します。これは平均値や合計などの統計値を計算するクエリです。今回は、合計を計算するためのオプション(.cumulativeSum)を指定します。

func fetchStepCount() {
    let stepType = HKQuantityType.quantityType(forIdentifier: .stepCount)!
    let startDate = Calendar.current.startOfDay(for: Date())
    let predicate = HKQuery.predicateForSamples(withStart: startDate, end: Date(), options: .strictStartDate)

    let query = HKStatisticsQuery(quantityType: stepType, quantitySamplePredicate: predicate, options: .cumulativeSum) { _, result, error in
        if let sum = result?.sumQuantity() {
            DispatchQueue.main.async {
                self.stepCount = sum.doubleValue(for: HKUnit.count())
            }
        }
    }
    healthStore.execute(query)
}

サンプルアプリでは、このクエリが返す合計値をstepCountプロパティに反映しています。

他の健康データ(心拍数や睡眠データ等)も、HealthKitが提供するクエリのオプションを変えることで簡単に取得できます。まずは基本的なクエリの使い方を習得して、徐々に応用してみてください。

データの同期や更新の遅延について

Apple WatchiPhone間でデータを同期する場合、バックグラウンドでデータのやり取りが行われます。基本的には、バッテリー消費やシステム負荷が考慮され適切なタイミングで更新が行われますが、以下の点は注意が必要だと思います。

  • 同期のタイミングについて
    両デバイスが適切と判断したタイミングで同期が行われるため、同期が遅延していると感じることがあるかもしれません。一方、 ユーザーの明示的なアクションに対しては即時にデータが更新されます。例えば同期ボタンを配置し、押されたタイミングでHealthKitデータの同期をトリガーすることが可能です。
  • バッテリー消費に注意
    データの頻繁な更新はバッテリー消費につながるため、必要に応じて更新頻度を見直すことも検討しましょう。例えばユーザーによる同期ボタンの連打を抑止するなどの対策は有効です。

まとめ

HealthKitを活用すれば、ユーザーの健康データを効率的に管理し、アプリの付加価値を高めることができます。さらに進化した健康管理アプリを目指していきましょう!

今回はHealthKitを用いた健康管理アプリ開発の概要と実装方法の一部をご紹介しました。ぜひこの内容を参考に、ご自身のアプリにHealthKitを取り入れてみてください。

参考

HealthKit | Apple Developer Documentation



書いた人:大嶋

女性にとって、自分の体調が「わかる」未来を目指して〜Ladynamicプロジェクトのご紹介〜

こんにちは、データサイエンスチームの瀬川です。

私は以前、病院で理学療法士として働いていました。
日々患者さんと接する中で感じたのは「自身の不調がより早期に発見でき、他者にもわかりやすく伝えられる」ことがとても重要であるということです。
それを可能にしたいという想いから、現在はデータサイエンティストの立場で、身体の状態を可視化し、誰もが自分の体調を理解しやすくなる未来を目指しています。

今回のTechBlog第六回では、私が進めている「女性の月経周期に関連するデータの可視化」に焦点を当て、その取り組みについてご紹介していきます。


「Ladynamic」プロジェクトと、女性ヘルスケアの課題について

弊社には女性メンバーのみで構成された「Ladynamic」というプロジェクトがあり、女性の視点に立った課題提起とデータ解析に取り組んでいます。

女性がキャリアを築きながら社会で活躍する機会はますます増えています。リーダーシップを発揮する場面や、専門分野での高い貢献が期待される場面も多く、さまざまな分野で女性がその力を発揮しています。

一方で見逃されがちなのが、女性特有の体調不良や疾患にまつわる問題です。
たとえば月経前症候群PMS)、月経困難症、さらには更年期症状など、月経周期に関わる症状は仕事や日常生活にも影響を及ぼします。
またもう一つの問題として、これらの症状に悩む女性は多いものの、その悩みを他者に相談することにためらいを感じたり、自分の症状を「たいしたことない」と思い込んでしまったりするケースも少なくありません。

このような課題に立ち向かうために、私たちが立ち上げたのが「Ladynamic」プロジェクトです。

このプロジェクト名は、「Lady(女性)」と「Dynamic(生き生きと)」の2つの言葉の組み合わせでできています。テックドクターの得意分野であるウェアラブルデータの活用とデジタルバイオマーカー(※)の開発という手段を用いて、女性がより健康的で生き生きとした生活を送れる未来を実現するためのプロジェクトです。

※デジタルバイオマーカー……ウェアラブルバイスで測定した『日常データ』をもとにした、病気の早期発見や治療につながる客観的指標(過去記事参照

スマートウォッチなどのウェアラブルバイスの強みは、長期間の連続した生体データを取得できる点にあります。測定の手間も、測定忘れの心配もありません。
ここで収集したデータを活用して、女性特有のデジタルバイオマーカーを開発することができれば、女性が自分の体調を予測・管理する大きな助けとなるはずです。

では具体的にどんなデータの解析を行っているのか、その一例をご紹介します。



女性に関連するデータ解析の紹介①:安静時の心拍数が月経周期と連動している?

まず、社内の女性1名に睡眠、脈拍、活動量に関するデータを数ヶ月にわたって蓄積してもらいました。

これに加え、被験者にはアンケートで月経開始日と終了日を記入してもらい、ウェアラブルバイスデータの推移と月経周期の関係性を可視化しました。

図1:ウェアラブルバイスデータと月経周期のグラフ。安静時心拍数は上段。

黄体期排卵後から次の月経が始まるまでの期間)を月経開始前の7日間、卵胞期(月経が終わってから排卵までの期間)を月経終了後の5日間と定義しました。
ウェアラブルバイスデータは1日単位に集約し、日ごとのデータとして使用しました。
※グラフに使用したデータは、1日のデータ取得率が70%を超えるもののみを採用しています。

月経期は背景を赤色、黄体期は青色、卵胞期は緑色で色分けしています。
注目してほしいのは一番上の折れ線グラフです。これは安静時心拍数の推移を表しています。
月経期が始まると月経終了日に向けて安静時心拍数が下がっていき、卵胞期を経て黄体期に向かって再度上がっていく周期性がわかるでしょうか。

月経周期に合わせて基礎体温が周期的に変動することは、一般的によく知られています。今回のグラフからは、安静時心拍数にも同じように月経周期に関連した周期性がある可能性が示唆されました。

女性に関連するデータ解析の紹介②:ウェアラブルバイスデータで月経痛の定量化ができる?

次にご紹介するのは、月経痛の定量化に関する取り組みです。
月経痛は個人差が大きいですが、それを定量化する手段が少ないことが課題です。

そこで私たちは社内の女性メンバーを対象に、月経痛が生じるごとに、月経痛に関するアンケートに回答してもらいました。
アンケートには、月経痛の強さに関する設問(MDQ(※)を参考に5段階評価)や、月経痛の持続時間の設問が含まれています。その回答から、痛みが軽く持続時間が短い人を「月経痛の軽い人」、痛みが強く長時間続く人を「月経痛の重い人」として定義しました。

MDQ……月経前や月経中にともなう症状を測定する尺度

次に、「月経痛の軽い人」と「月経痛の重い人」を1名ずつ選出し、それぞれの月経期(痛み無し)/月経期(痛み有り)/卵胞期/黄体期の期間に、ウェアラブルバイスデータの違いがあるかどうかを箱ひげ図で可視化しました。

図2(a) 「月経痛が重い人」の期間別箱ひげ図
図2(b) 「月経痛が軽い人」の期間別箱ひげ図

※アンケートとウェアラブルバイスのデータは3周期分のデータを使用しました。

月経期においては、「月経痛の重い人」では痛み無し期よりも痛み有り期の安静時心拍数が高い傾向があります。しかし「月経痛の軽い人」ではそれが逆転しています。

また、両者ともに黄体期は卵胞期よりも安静時心拍数が高いですが、「月経痛の重い人」は「月経痛の軽い人」よりも安静時心拍数の変動が大きい傾向がみられました。

今回の解析は「月経痛の重い人」と「月経痛の軽い人」の被験者数が1名ずつであったため、統計的な検定は行わず、可視化に留めました。
しかし今回の結果から期間の安静時心拍数の変動の大きさが月経痛の強さによって違う可能性が見えてきました。

今後は、より多くの被験者を対象にしてデータを集め、統計検定を行いながら月経痛とウェアラブルバイスデータの関係をより詳しく分析していきたいと考えています。


女性が自分の体調を予測・管理し、より健康的で生き生きとした生活を送ることができる未来を目指して

今回は月経周期に関連するウェアラブルバイスデータの可視化について書きましたが、今後はウェアラブルバイスデータから月経開始日や排卵日を予測することにも取り組んでいく予定です。

ウェアラブルバイスで予測が可能になれば、現在広く使われている基礎体温測定の代わりとして、より手軽で正確な月経周期管理が実現できるかもしれません。

私たち「Ladynamic」プロジェクトが目指しているのは、ウェアラブルバイスを通じて自身の体調を把握することで、より多くの女性が自分の体調を理解し対処することができる未来です。
また「自分の症状が軽いのか重いのか分からない」といった悩みに対しても、個人の症状を他者と比較することができれば、必要なケアを受けやすくなるはずです。

今回ご紹介した以外にも、今後は月経前症候群PMS)や月経困難症などの疾患に対するデジタルバイオマーカー開発にも取り組んでいく予定です。

こうした活動を通して、わたしたちはより女性が安心して活躍できる社会の実現を目指していきます。



書いた人:瀬川

Next.js(App router)における開発しやすいディレクトリ構成の例

初めまして、テックドクターでフロントエンド開発を担当している大瀧です。

ディレクトリ構成はコードの可読性やスケーラビリティに関わる重要な要素であると思っています。

しかし、フロントエンドのディレクトリ構成はベストプラクティスが確立されておらず、わりと悩むポイントです。

そこで今回は、Next.jsのApp routerにおいて、弊社で採用しているディレクトリ構成を共有します。この記事がディレクトリ構成に悩む開発者の助けになれば幸いです。

ディレクトリ構成の自由度が高すぎる問題

さきほど「フロントエンドのディレクトリ構成はベストプラクティスが確立されていない」と書きましたが、特にApp routerのディレクトリ構成については、公式ドキュメントで以下のように記載されています。

There is no "right" or "wrong" way when it comes to organizing your own files and folders in a Next.js project.
 
Next.js プロジェクトでファイルやフォルダを整理する方法には「正しい」「間違っている」はありません。

引用元:Routing: Project Organization | Next.js(日本語訳:プロジェクト構成とファイル配置 | Next.js 公式ドキュメント 日本語翻訳プロジェクト

正解も不正解もないと明記されているものの、これは自由度を高める一方で、開発者からすると悩ましいです。実際のプロジェクトにおいては、ある程度ルール化しておかなければコードの品質が保てませんし、保守しづらいコードは開発速度低下を招いてしまいます。

そこで本記事では、テックドクターで私が手がけているHealth Portalというプロダクトの構成を共有します。Health Portalは、クライアント(ヘルスケアアプリやSaMDを開発したい企業)がデジタルバイオマーカー*1を利活用するためのプラットフォームです。JavaScriptフレームワークにNext.js(App router)を採用しています。

Health Portalの技術スタック

前提として、フロントエンドで採用している主なライブラリはざっくり以下のとおりです。

  • Next.js v14
  • Tailwind CSS
  • React Hook Form
  • zod
  • keycloak.js

またAPIはFastAPIで実装しており、通信プロトコルにはREST APIを採用しています。

弊社のディレクトリ構成とその解説

ディレクトリ構成と各ディレクトリの役割

まず、ディレクトリ構成の全体像は以下のとおりです。

src
|
+-- app        # Routing files(Next.jsが標準で提供するファイル)
|
+-- components # 横断的(ドメインに依存しない)なUIコンポーネント
|
+-- features   # 特定のドメイン・機能に関係するコンポーネント
|
+-- hooks      # ドメインに依存しない、横断的なhooks
|
+-- providers  # アプリケーションプロバイダー
|
+-- utils      # 横断的な汎用関数
|
+-- constants  # 横断的な定数
|
+-- types      # 横断的な型定義
|
+-- styles     # スタイリング(css)に関するファイル
|
+-- lib        # ライブラリの処理や標準処理を共通化したコード
|
+-- tests      # 自動テスト関連

 

  • app: ルーティングの責務を持つディレクトリ。Routing files(Next.jsが標準で提供するファイル。page.tsxやlayout.tsxなど)のみを配置します
  • components: ボタンやモーダルなど、再利用可能でドメインに依存しないUIコンポーネントを格納します。
  • features: 特定の機能に関連するファイルを集約し、機能ごとのサブディレクトリに格納しています。例えば、ユーザー管理機能に関連するコンポーネント、hooks、ユーティリティ関数、型定義などをこのディレクトリ内でまとめます。
  • hooks: グローバルに使用されるカスタムフックを格納します。ドメインに依存しない汎用的なロジックはここにまとめます。
  • providers: ReactのContext APIやその他のプロバイダーを配置します。グローバルな状態管理やテーマ設定など、アプリケーション全体で使用されるプロバイダーをここに集約します。
  • utils: 簡潔で再利用可能なユーティリティ関数を格納します。これには、日付処理、文字列操作、API通信など、特定のドメインに依存しない関数が含まれます。
  • constants: アプリケーション全体で使用される定数を格納します。
  • types: TypeScriptの型定義をまとめます。
  • styles: グローバルスタイルやテーマ設定、Tailwind CSSのカスタマイズを含むスタイルシートをここに配置します。
  • lib: プロジェクト内で再利用されるライブラリや、外部ライブラリの実装をここに格納します。
  • tests: ユニットテストや統合テストなど、自動テストに関連するファイルを格納します。

基本的には、Reactアプリ開発のベストプラクティスと言われるbulletproof-reactを参考にしてディレクトリ構成を考えています。

bulletproof-reactについてはmejinさんの記事がとてもわかりやすくまとまっているのでご参照ください。
Reactベストプラクティスの宝庫!「bulletproof-react」が勉強になりすぎる件

肝となるのがfeaturesディレクトリで、ここには機能ごとのファイルが格納されます(詳細は後述)

以前はコンポーネントの粒度毎にディレクトリを分割するAtomic Designが流行ってた印象ですが、ドメイン毎にディレクトリを分類するほうが、フレームワークへの依存度が少なく学習コストやフレームワークの移行ハードルが低いように思います。

またClean Architectureの著者であるRobert C. Martinさんも記事の中で以下のように述べています。

Your architectures should tell readers about the system, not about the frameworks you used in your system.
 
あなたのアーキテクチャが読み手に伝えるべきなのはシステムのことであって、使用したフレームワークのことではありません。

引用元:Clean Coder Blog(日本語訳は筆者)

featuresの切り方について

featuresディレクトリの構成は、名のとおり機能毎にディレクトリを分割しています。

ディレクトリ分割の判断基準としては、オブジェクト指向UIというUI設計思想に基づき、分類しています。 オブジェクト指向UIとは、簡単にいうとオブジェクトを起点にユーザーのアクションが設計されたUIのことで、ここでいうオブジェクトとは「ユーザーが関心を寄せるモノ、ユーザーの操作対象となる名詞」を指します。このオブジェクト(ユーザーの関心事)ごとにディレクトリを分割しています。

例えば、「ユーザーの作成」や「ユーザーの削除」等のアクションがある場合、オブジェクトは「ユーザー」となり、関連するソースコードはuserディレクトリに格納することになります。 オブジェクト指向UIの詳細については、ぜひこちらを読んでみてください。

オブジェクトごとのディレクトリ配下の構造についても、基本的にbulletproof-reactに則っており、下記の図のとおり特定のドメインに依存するコンポーネントを格納します。

features            
|
+-- user            # 特定のドメインに関するディレクトリ
|   |
|   +-- hooks       # 特定のドメインに関するhooks
|   +-- utils       # 特定のドメインに関する汎用関数
|   +-- components  # 特定のドメインに関するコンポーネント
|   +-- types       # 特定のドメインに関する型
|   +-- providers   # 特定のドメインに関するプロバイダー
|   +-- pages       # appディレクトリ配下から呼び出されるページコンポーネントを格納する
|       +-- users_page.tsx
|       +-- user_new_page.tsx
|       +-- ...
+-- ...

しかし、オブジェクトごとのディレクトリすべてが完全に同じ構造を持つ必要はなく、その機能に必要なディレクトリのみを作成します。

appディレクトリとfeaturesディレクトリの対応関係について

features/xxx/pagesに格納されたページコンポーネントがエントリポイントとなり、appディレクトリ配下の対応するpage.tsxから呼び出されます。

app                 
|
+-- users            
|   | 
|   +-- page.tsx     # `features/user/users_page.tsx`を呼び出す
|
features
|
+-- user        
|   |
|   +-- pages 
|       +-- users_page.tsx
|       +-- ...
+-- ...

細かなルールについて

異なるfeature間での依存はNG

各featureは独立して機能するべきであり、他のfeatureに依存しないことで低結合・高凝集なソースコードを実現することができるため、異なるfeature間での依存はNGとしています。

横断的なコンポーネントに特定ドメインの要素を含ませるのはNG

コンポーネントを汎用化し、かつfeatureの凝集度を高めるために、横断的なコンポーネントに特定ドメインの要素を含めるのはNGとしています。

簡単な例ですが、以下のようにButtonコンポーネントにuserドメインを含めると汎用性が落ちてしまいます。

type ButtonProps = {
  user: {
    role: 'admin' | 'user';
  };
} & ButtonHTMLAttributes<HTMLButtonElement>;

const Button: FC<PropsWithChildren<ButtonProps>> = ({ user, children, ...props }) => {
  const buttonClass = user.role === 'admin' ? 'bg-red-500' : 'bg-blue-500';

  return (
    <button {...props} className={buttonClass}>
      {children}
    </button>
  );
};
ファイル名にパスカルケース・キャメルケースを使用しない

MacWindowsではファイル名の大文字小文字を区別しませんが、Linuxではこれを区別するのでファイル名はスネークケースで統一してます。

本構成のメリット

コードリーディング時の認知負荷が下がる

単一の機能毎にディレクトリが分かれているため、コンテキストが統一されコードの理解がし易いと個人的には思っています。

他の開発者とコンフリクトしづらくなる

機能毎にディレクトリが分かれているため、機能修正や追加の際にコンフリクトが起きにくくなります。大規模な開発体制の場合は開発生産性が結構上がるんじゃないかと思います。

フロントエンドエンジニア以外も多少コードリーディングが楽になる

これは実際に確認したわけではないのですが、フロントエンド特有のしきたりではなくドメイン毎にディレクトリが分かれているため、プロダクトそのものの知識があればコードリーディングが楽になるような気がしています。

デメリット・改善点

やっぱりfeaturesディレクトリ配下の切り方が曖昧

オブジェクト指向UIの考え方を参考にfeatureを分類することで、開発者間で一応は共通認識を持つことは出来ているような気はしつつ、明確な線引があるわけではないのでたまに悩んでしまいます。ラクスさんの場合は画面仕様書単位で切り分けているようです。

最終的な画面表示のロジックはfeatures/xxx/pagesではなく、app配下で実装してもいいかも

features/xxx/pagesを作成すると責務の分離ができる反面、該当のページがどのURLに対応するかが分かりづらくなってしまいます。
なので、pageのレンダリング処理に関してはapp配下で実装してもいいかもしれないです。
実際、元VercelのエンジニアであるSteven Teyさんが開発している、dubというプロダクトではapp配下にpage.tsxpage-client.tsxを格納する設計になってます(以下を参照ください)
dub/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/tokens at 35973de925857f4952d7910a2424ffab478ad2cb · dubinc/dub · GitHub

正直なところ、ルーティングとレンダリングの責務を分離することに今のところ大きなメリットを感じていないので、こっちのほうが分かりやすいかも知れません。

*1:ウェアラブル端末やスマホから取得できるデジタルの情報をもとに、病気や治療の変化を可視化するための指標

データ解析で伝える「調子」~心拍・運動・睡眠データから得られる意味

こんにちは。データサイエンスチームの坂本と申します。TechBlog第四回では、日常生活中の心拍や睡眠などのデータを解析する意義や可能性について、分析者の視点からお話しします。

私たちは、スマートウォッチを代表とするウェアラブル端末で測定されたデータの解析を行っています。このデータ解析という行為を少し抽象的に言い換えると、「数字の羅列から意味を見つけ出す技術」であると表現できます。

そこから得られる「意味」とは、いったい何であり、何の役に立つのでしょうか?

心や身体の「調子のバロメータ

私たちが主に取り扱う心拍・運動・睡眠のデータ*1は、いわば心や身体の「調子のバロメータ」です。

これは体温計のようなものだと考えると馴染みやすいかもしれません。37度の微熱だとか、39度近い高熱だといったように、自分の体調やしんどさを家族やお医者さんに伝える時の「言葉」になるものです。

もちろん、体温だけではわからないことも多いです。ですが咳が出る等の周辺の情報と組み合わせることで、その人の心や身体の状態への理解が深まり、病気の診断や、適切な薬の処方に貢献します。

デジタル生体データも、これによく似ています。自分の体の変化を数字でとらえることで、客観的に体調の変化を表すことができます。

病気と健康の「第三の情報源」

これまで、心身の調子は主に「本人の感覚」や「病院での検査」などで捉えられてきました。もちろん現在も、これらは様々な研究や病気の診断などで、最も重要なものとして考慮されています。

しかしその一方で、本人でさえ気づいていない心身の変化や、病院では見えてこない日常生活のしんどさといったものも存在します。それらは見落とされたり、診察の時間中だけで伝えきることが難しいために取りこぼされてきました。実はここに、「本来必要な情報の不足」が発生しています。

デジタル生体データは、いろいろな条件でデータを抽出したりすることで、本人も気づかないような「日々の調子」を捉えることができます。例えば「眠っている時に心拍数が急激に変化している」などは、本人であっても自覚が困難ですが、病気の診断や予防に重要な役割を果たしそうですよね。私たちデータ解析者は、こういった情報をうまく抽出し、わかりやすく活用できるよう、様々な解析をしています。ひとつひとつの開発と検証が進む中で、デジタル生体データは、「病気と健康の第三の情報源」として確立したものになってゆくだろうと予感しています。

図1. デジタル生体データは病気と健康の第三の情報源

データ解析の面白さと様々な工夫

さきほど、デジタル生体データは調子のバロメータであり、体温計のようなものであると表現しました。しかし、体温計の場合、「熱がある」というだけでは、原因が何かまではわかりません。

他方、デジタル生体データの場合、「心拍数が高くなっている」という情報から、その背景理由を推測したり、将来起きる出来事を予測できる場合があります。心拍数が上がる理由の代表例は「運動」「飲酒」「発熱」などですが、細かく分析を進めてゆくと、心拍数の「上昇のしかた」が全て異なっているということを発見(!)したりします。

データ解析では、こういった心拍数の「上昇のしかた」を特定し、データを上手に加工することで、「運動」「飲酒」「発熱」を判別できるようにしたりします。すると、「先月、何日間ほど発熱で寝込んでいたか」や「1ヶ月〜1年でどれくらいお酒を飲んでいたか」が分かるようになります。「風邪を引いて治ったと思っていたけれど、まだ完全には身体の状態が戻っていなかった」ということにも気がついたりします。こういった技術は、本人が生活を振り返ったり、誰かに伝える場合だけでなく、研究などにも応用できると考えられます。

図2. 発熱に反応する心拍バロメータの例

また、スマートウォッチ等で測定されるデジタル生体データには、新しい第三の情報源としての利点がある一方で、いくつかの弱点もあります。着脱などによる測定データの欠落がその一例です。こういったデジタル生体データの特徴に対して、データ解析の視点からは「解析結果の品質を守るための取り組み」が必要になります。

例えば「睡眠不足」という情報を抽出したい時には、毎日の睡眠データが必要になります。しかし、寝ている時にスマートウォッチを外した日や、睡眠中に電池が切れてしまった時には、データの欠落が発生してしまいます。このような場合、データ解析者は「欠損補完」という技術でデータや解析結果の品質をカバーしようと試みることがあります。

欠損補完にも様々な手法が存在し、ケースバイケースで最適な方法が異なります。そこで、「わざとデータを欠落させて、欠損補完をした時の復元度合いを確認する」といった方法で、最適な欠損補完方法を検証したりしています。

図3. 欠損補完の性能比較(一部の手法のみ掲載)

このように生体データ解析では、工夫すればするほど様々な価値が出てきます。株式会社テックドクターには、心理学を勉強してきた私を一例として、様々なバックグラウンド/専門分野を持つデータサイエンティストが所属しています。それぞれの分析者が持つユニークな視点や、得意な技術を活かすことができる領域であり、そんなところも魅力のひとつです。

デジタル生体データの解析は可能性の宝庫

ここまでで紹介できたのはほんの一例に過ぎませんが、デジタル生体データの解析は可能性に満ちています。これからたくさんの発見があり、一つ一つの発見が実用化につながってゆくと予感しています。

実用化の領域は、一人ひとりが関心を持っている事柄(執筆者の場合は、ランニングで心肺機能が回復したかを確認したりすること)をセルフチェックしたり、病院での診察場面や、病気の予防や治療効果に関する研究など、様々だと考えられます。どのような応用例であっても、データや分析結果の品質を守る取り組みは、丁寧に進めていきたいですね。

自分自身の心身の調子を、家族や友人、お医者さんに話すとき。あるいは、研究という取組を通じて、一人ひとりの体験を世界に伝えてゆくときに。デジタル生体データが、「人と人とが理解し合うための言葉」として使われることを願っています。


書いた人:坂本

*1:※ 他にも、様々な種類のデジタル生体指標や、検査データ、アンケートデータなどを幅広く取り扱っています。

B+treeで効率良く後方一致検索をする方法

初めまして、テックドクターのバックエンドエンジニアの魚木です。

私が担当するプロジェクトに、データベースのテーブルのあるカラムを前方一致検索する機能があります。そこに部分・後方一致検索もしたいという要望がありました。

そのデータベースはB+treeインデックスが使用されていますが、B+tree (または同系統のtree) インデックスは部分・後方一致検索は効率良くできないと言われています。
結果的に機能追加は見送られたのですが、前方・部分・後方一致検索の違いについて考えるよい機会になりました。

本記事は、B+treeが部分・後方一致検索を効率よくできない理由と、その代替手段として後方一致検索を高効率でする方法を説明します。

MySQLを前提としますが、インデックスの構造が同系統であれば同じような結論になります。

なぜ前方一致検索のみ高効率なのか

まずはtree構造が前方一致検索を効率よく処理できるしくみを説明します。ここではシンプルに、二分探索木で考えてみましょう。

下記のような二分探索木があるとします。

  • 葉ノードのみ実データを持ち、それ以外のノードはキーのみを持つ
  • 隣接する葉ノードは相互にリンクされている
  • キーは8個存在し、それぞれ "aec", "afd", "agc", "ahd", "bec", "bfd", "bgc", "bhd" のデータを持っており、辞書順でソート済みであるものとする。

図にすると、このようになります。

図1:二分探索木の例

完全一致検索の場合

まず、完全一致検索について考えます。完全一致検索は前方一致検索の一種であるため、高効率に処理することができるはずです。

上記のtreeから、あるキーを持つ葉ノードを検索した場合……

  1. 根ノードの "ahd" と比較し、入力が "ahd" 以下であれば左のブランチを進みます。"ahd" より大きければ右のブランチを進みます。1つ階層を降りた時点で、入力と同じキーをもつ可能性のある葉ノードは8個から4個に絞られます。
  2. 同様の操作を深さ1のノードで行うと、候補がさらに4個から2個に絞られ、
  3. 同様の操作を深さ2のノードで行うと入力と同じキーを持つノードが特定されるか、存在しないことが判明します。
図2:二分探索木の完全一致検索の様子(ここでは "bec" を検索)

このように、treeの階層をひとつ降りるたびに候補が1/2ずつに絞られることが二分探索木の探索の効率の良さを生んでいます。

データベースでよく使われるB+treeは各ノードの保持するキーが複数になりますが、階層を降りる毎に候補が1/n (二分探索木の場合はnが2) に絞られるという点で同じです。

前方一致検索の場合

次に前方一致検索について考えます。

同じtreeで、"b" から始まるデータを全て取得したいとします。

下記の手順で、"b" から始まるキーを持つ葉ノードの中で、辞書順で一番早いものを1回の探索で特定することができます。

  1. 最初は根ノードで "ahd" と "b" を比較し、"b" の方が辞書順で遅いので右のブランチを進みます。
  2. 次は "bfd" と "b" の比較で "b" の方が辞書順で早いので左のブランチを進み、
  3. 同様に "bec" のノードでも左のブランチを進むことで左から5番目の葉ノードである "bec" が特定されます。
図3:前方一致検索の様子("b" から始まるデータを検索)

そこから図3の緑色の矢印のように、隣接ノードへのリンクを右方向に、"b" から始まらないキーを持つ葉ノードが出現するか (※)、終わりまで走査し終わるかのいずれかになるまで進みます。ここでは "bec"、"bfd"、"bgc"、"bhd" の4件が該当しました。

※葉ノードが辞書順でソート済みであるので、一度 "b" 以外から始まる葉ノードに到達したら、それ以降に "b" から始まる葉ノードは絶対に存在しません


今回は1文字の前方一致の例を示しましたが、前方一致検索の条件が2文字以上になっても基本的な考えは変わりません。肝心なことは、前方一致する辞書順で一番早いキーを持つ葉ノードを1回の探索で特定できることと、葉ノードの並びが辞書順でソート済みであるため効率的に処理できるということです。

後方一致検索の場合

最後に後方一致検索について考えてみます。

例えば "c" で終わるキーを持つ葉ノードを取得したいとしましょう。

上で説明したように、根ノードから左に降りることは条件に合致する候補を1~4番目に限定することです。また、右に降りることは5~8番目に限定することを意味します。

しかし、下の図4からわかるとおり、"c" で終わる葉ノードは左から1、3、5、7番目に存在し、どちらに降りたとしても条件に合致するキーが反対側にも存在することになってしまいます。

とりあえず、全てのノードで左に進んだとしましょう。偶然、一番左の葉ノードのキーが "aec" と "c" で終わっているので、前方一致検索と同様にそこから右のリンクを進んでみましょう。そうすると次の葉ノードのキーは "afd" であり "c" で終わりませんが、それで走査を打ち切れるでしょうか?

 先に述べたように左から3、5、7番目にも "c" で終わるキーを持つ葉ノードが存在するので、走査を打ち切ってしまうと正しい結果は得られません。

図4:後方一致検索は効率的にできない

別のデータセットであれば、偶然1番目にしか "c" で終わるキーを持つ葉ノードが存在しないこともあるかもしれませんが、いずれにせよ葉ノードを全て調べて結果的にわかることであって、tree構造自体が保証するものではありません。

これらのことから、後方一致検索は葉ノードを全走査しなければならず、大抵の場合とても非効率になります。部分一致検索も同じ理由で非効率になります。

(実際にはこの例のようにデータ数が少ない場合は全走査した方が効率が良い可能性が高いですが、データ数が増えたときのことを想像してみてください)

B+treeで高効率で (擬似的に) 後方一致検索をする方法

これまで述べたように、B+tree (または同系統のtree) で後方一致検索を効率よく行うことは原理的に不可能です。しかし、B+treeで後方一致検索と同じ結果を高効率で得る方法もあります。以下のような手順で実現することができます。

  1. 後方一致検索をしたいカラムを反転したカラムを追加する
    • Generated Column (本記事はこれを使う)
    • トリガー
    • ...
  2. 反転したカラムのインデックスを作成する
  3. 後方一致検索対象の文字列を反転し、そのカラムを条件に前方一致検索する

後方一致検索をしたいカラムを反転したカラムを作成し、それに対してインデックスを作成することによって、後方一致検索を前方一致検索に変換することができます。上で説明したように前方一致検索は高効率で行えるので、事実上の後方一致検索を高速に行うことができます。

実際にやってみる

今回はGenerated Columnを使った方法で説明します。(後方一致検索をしたいカラムを反転したカラムさえあればいいので、トリガーでも実現可能です。)

Generated Columnの詳細は公式ドキュメントを参照していただきたいですが、ざっくりいうとカラム定義に式を使えるようにする機能です。

例えば、メールアドレスを保存するカラムが元からあるとして、それを参照しつつ反転したカラムを作成することができます。

このカラムは、参照元カラムの更新も反映されますし、インデックスを作ることもできます。

例として以下のような`email`カラムと`reverse_email`カラムを持ったusersテーブルを作成します。

CREATE TABLE users (
    id INT AUTO_INCREMENT,
    email VARCHAR(255) UNIQUE,
    reverse_email VARCHAR(255) GENERATED ALWAYS AS (REVERSE(email)) VIRTUAL,
    PRIMARY KEY (id),
    index email_index(email),
    index reverse_emai_index(reverse_email)
);

INSERT INTO users (email) VALUES
    ('test1@test.com'),
    ('test2@test.jp'),
    ('test3@test.com'),
    ('test4@test.jp'),
    ('test5@test.com'),
    ('test6@test.jp'),
    ('test7@test.com'),
    ('test8@test.jp')
;

SELECT * FROM users;
+----+----------------+----------------+
| id | email          | reverse_email  |
+----+----------------+----------------+1 | test1@test.com | moc.tset@1tset |
|  2 | test2@test.jp  | pj.tset@2tset  |
|  3 | test3@test.com | moc.tset@3tset |
|  4 | test4@test.jp  | pj.tset@4tset  |
|  5 | test5@test.com | moc.tset@5tset |
|  6 | test6@test.jp  | pj.tset@6tset  |
|  7 | test7@test.com | moc.tset@7tset |
|  8 | test8@test.jp  | pj.tset@8tset  |
+----+----------------+----------------+
8 rows in set (0.00 sec)

emailreverse_emailに対するインデックスを作成しており、SELECTの結果から想定通りにreverse_emailemailを反転したものであることがわかります。

まずは、emailの「.com」後方一致検索をしてみます。

SELECT * FROM users WHERE email LIKE '%.com';
+----+----------------+----------------+
| id | email          | reverse_email  |
+----+----------------+----------------+1 | test1@test.com | moc.tset@1tset |
|  3 | test3@test.com | moc.tset@3tset |
|  5 | test5@test.com | moc.tset@5tset |
|  7 | test7@test.com | moc.tset@7tset |
+----+----------------+----------------+
4 rows in set (0.09 sec)

EXPLAIN SELECT * FROM users WHERE email LIKE '%.com'\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: users
   partitions: NULL
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 8
     filtered: 12.50
        Extra: Using where
1 row in set, 1 warning (0.01 sec)

取得結果は正しいですが、EXPLAINのtypeをみるとALLとなっており、全表走査していることが分かります。

次に、後方一致検索用の文字列を反転し前方一致検索用の文字列にして、reverse_emailに対して検索します。

SELECT * FROM users WHERE reverse_email LIKE REVERSE('%.com');
+----+----------------+----------------+
| id | email          | reverse_email  |
+----+----------------+----------------+1 | test1@test.com | moc.tset@1tset |
|  3 | test3@test.com | moc.tset@3tset |
|  5 | test5@test.com | moc.tset@5tset |
|  7 | test7@test.com | moc.tset@7tset |
+----+----------------+----------------+
4 rows in set (0.13 sec)

EXPLAIN SELECT * FROM users WHERE reverse_email LIKE REVERSE('%.com')\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: users
   partitions: NULL
         type: range
possible_keys: reverse_emai_index
          key: reverse_emai_index
      key_len: 1023
          ref: NULL
         rows: 4
     filtered: 100.00
        Extra: Using where
1 row in set, 1 warning (0.07 sec)

emailに対する後方一致検索と同じ結果が得られました。また、EXPLAINのtypeがrange、keyがreverse_email_indexとなっていて、インデックスを使っていることがわかります。

このように、Generated Columnを使えば後方一致検索を高効率で行うことができます。

(例ではデータ数が小さいので、emailに対する後方一致検索の方が早いですが、データ数が大きくなるとreverse_emailに対する前方一致検索の方が早くなります)

注意点

この方法で事実上の後方一致検索を高効率でできますが、別のコストが増加する場合もあるので注意が必要です。

  • インデックスの追加による更新系クエリの性能の悪化
  • アプリケーションで後方一致検索の要求をうまく処理する必要がある

実装にあたってはこれらのコストを考慮して、本当に後方一致検索が必要かどうかを判断する必要があります。

ちなみに部分一致検索はこの方法ではできませんし、私の知る限りB+tree (または同系統のtree) で高効率で部分一致検索をする方法はありません。



この記事を読んで、少しでもテックドクターに興味を持っていただけたら嬉しく思います。
今後も技術系の記事を書いていく予定なので、よろしくお願いします。


書いた人:魚木