TECH PLAY

タイミー

タイミー の技術ブログ

299

こんにちは、データ統括部データサイエンス(以下DS)グループ所属の小関 ( @ozeshun )です。 本記事では、タイミーで取り組んでいるレコメンドに使用する アルゴリズム を検証する際に活用した、RecBoleでの実験方法について紹介したいと思います。 ※ Timee Advent Calendar2023 の12月8日分の記事です。 RecBoleとは RecBoleを活用したアルゴリズムの実験手順 0. ディレクトリ構成 1. 学習データの準備とRecBoleで使用するconfig fileの用意 2. 学習データをAtomic file *3 へ変換 3. モデルの学習 4. 学習したモデルの検証 おわりに We’re Hiring! RecBoleとは RecBole とは、レコメンド アルゴリズム を統一されたインターフェースで提供する事を目的としたプロジェクトであり、後述のように アルゴリズム 間の比較を簡単に実現出来ます。2023/12/8現在、91種類の アルゴリズム が実装されており、 Python のライブラリ *1 として公開されています。 実装されている アルゴリズム は、 Model Introduction から確認できます。 今回は、実装されている アルゴリズム の中でもexplicitなフィードバックを予測すること *2 を目的とした、 Context-aware Recommendation の アルゴリズム をRecBoleを使用して検証する一連の流れを紹介したいと思います。 RecBoleを活用した アルゴリズム の実験手順 0. ディレクト リ構成 今回は、以下のような ディレクト リ構成の元、ノートブック上で実験を進める手順を説明します。 ├── notebook.ipynb ├── artifact │ ├── saved # 学習したモデルの保存先 │ │ ├── AFM-%m-%d-%Y_%H-%M-%S.pth │ │ ├── DeepFM-%m-%d-%Y_%H-%M-%S.pth │ │ ├── FM-%m-%d-%Y_%H-%M-%S.pth │ │ ├── NFM-%m-%d-%Y_%H-%M-%S.pth │ └── train_data # 学習用のデータの保存先. pickle fileは、Atomic fileに変換する際のソース. │ ├── interact.pkl │ ├── items.pkl │ ├── users.pkl │ ├── train_dataset # RecBoleが学習で使用する、Atomic fileの保存先 │ ├── train_dataset.inter │ ├── train_dataset.item │ └── train_dataset.user ├── base_dataset.py # データセットのI/Oをコントロールするクラスのベース (詳細はStep.2に記述) ├── config ├── model.hyper # 探索したいハイパーパラメータと探索範囲を記述したファイル └── train.yaml # RecBoleで使用する、学習方法などを記述したconfig file 1. 学習データの準備とRecBoleで使用するconfig fileの用意 学習用データの準備 今回は、Context-awareなモデルを学習することを目的としているので、下記のように、user_id、item_id、explicitなフィードバックを表すカラム、ユーザー・アイテムの特徴量を含む pandas.DataFrame 形式のオリジナルデータを用意します。   user_id item_id target user_feature item_feature 1 1 0 0 -1.0 2000 2 1 2 1 0.5 3000 3 2 1 1 -0.8 4000 4 2 2 0 0.0 5000 RecBole用のconfig fileの用意 このファイルには、データの保存先などの環境の設定、使用するデータに関する情報、学習方法や評価方法の設定を記述します。 # config/train.yaml # 使用するモデル名とデータセット名を指定----------------------------------- model : FM dataset : train_dataset # Environment Settings----------------------------------------------- # https://recbole.io/docs/user_guide/config/environment_settings.html gpu_id : 0 use_gpu : False seed : 2023 state : INFO reproducibility : True data_path : 'artifact/train_data/' checkpoint_dir : 'artifact/saved/' show_progress : True save_dataset : True save_dataloaders : False # Data Settings------------------------------------------------------ # https://recbole.io/docs/user_guide/config/data_settings.html # Atomic File Format field_separator : " \t " seq_separator : "@" # Common Features USER_ID_FIELD : user_id ITEM_ID_FIELD : item_id LABEL_FIELD : target # Selectively Loading load_col : # interaction inter : [ user_id, item_id, target ] # ユーザー特徴量 user : [ user_id, user_feature, ] # アイテム特徴量 item : [ item_id, item_feature, ] # Preprocessing # 標準化する特徴量を指定 normalize_field : [ item_feature, ] # Training Setting--------------------------------------------------- # https://recbole.io/docs/user_guide/config/training_settings.html epochs : 100 train_batch_size : 1024 learner : 'adam' train_neg_sample_args : ~ eval_step : 1 stopping_step : 3 loss_decimal_place : 4 weight_decay : 0 # Evaluation Settings------------------------------------------------ # https://recbole.io/docs/user_guide/config/evaluation_settings.html eval_args : group_by : user split : { 'RS' : [ 0.8 , 0.1 , 0.1 ]} mode : labeled repeatable : True metrics : [ 'LogLoss' , 'AUC' ] topk : 20 valid_metric : LogLoss eval_batch_size : 1024 metric_decimal_place : 4 eval_neg_sample_args : ~ 次のステップでAtomic fileに変換する際のソースとなるように、用意したDataFrameをpkl形式で保存 下記のコードをノートブック上で実行すると、ユーザー×アイテムのインタ ラク ション、ユーザーの特徴量、アイテムの特徴量を抽出したデー タセット が ARTIFACT_PATH 配下に保存されます。 import os import yaml import pandas as pd # データの読み込み (データソースはなんでも良い) train_df = pd.read_csv( '/path/to/train.csv' ) # yaml形式で書かれたRecBoleのcofig fileを読み込む TRAIN_YAML_PATH = 'config/train.yaml' with open (TRAIN_YAML_PATH, 'r' ) as yaml_file: train_config = yaml.safe_load(yaml_file) # cofig fileから各データセットに使用するカラム名を抽出 INTERACTION_COLUMNS = train_config[ 'load_col' ][ 'inter' ] USER_COLUMNS = train_config[ 'load_col' ][ 'user' ] ITEM_COLUMNS = train_config[ 'load_col' ][ 'item' ] TOKEN_COLUMNS = [ 'item_id' , 'user_id' , ] # Atomic fileに変換する際のソースデータとしてpkl形式で保存 ARTIFACT_PATH = 'artifact/train_data/' train_df[INTERACTION_COLUMNS].to_pickle(os.path.join(ARTIFACT_PATH, 'interact.pkl' )) train_df[ITEM_COLUMNS].to_pickle(os.path.join(ARTIFACT_PATH, 'items.pkl' )) train_df[USER_COLUMNS].to_pickle(os.path.join(ARTIFACT_PATH, 'users.pkl' )) 2. 学習データをAtomic file *3 へ変換 変換時に使用する、デー タセット のI/Oをコン トロール するクラスをノートブック上に記述 このクラスには、インプット・アウトプット先や、使用するカラムの情報とそのデータ型を記述しておきます。 # https://github.com/RUCAIBox/RecSysDatasets/blob/master/conversion_tools/src/base_dataset.py をそのままimportして継承 from base_dataset import BaseDataset class TrainDataset (BaseDataset): def __init__ (self, input_path, output_path): super (TrainDataset, self).__init__(input_path, output_path) self.dataset_name = 'train_dataset' # input_path self.inter_file = os.path.join(self.input_path, 'interact.pkl' ) self.item_file = os.path.join(self.input_path, 'items.pkl' ) self.user_file = os.path.join(self.input_path, 'users.pkl' ) self.sep = ',' # output_path output_files = self.get_output_files() self.output_inter_file = output_files[ 0 ] self.output_item_file = output_files[ 1 ] self.output_user_file = output_files[ 2 ] # selected feature fields inter_fields = { i: f '{c}:token' if c in TOKEN_COLUMNS else f '{c}:float' for i, c in enumerate (INTERACTION_COLUMNS) } item_fields = {i: f '{c}:token' if c in TOKEN_COLUMNS else f '{c}:float' for i, c in enumerate (OFFER_COLUMNS)} user_fields = {i: f '{c}:token' if c in TOKEN_COLUMNS else f '{c}:float' for i, c in enumerate (USER_COLUMNS)} self.inter_fields = inter_fields self.item_fields = item_fields self.user_fields = user_fields def load_inter_data (self): return pd.read_pickle(self.inter_file) def load_item_data (self): return pd.read_pickle(self.item_file) def load_user_data (self): return pd.read_pickle(self.user_file) 下記コードをノートブック上で実行して、 ARTIFACT_PATH 配下に格納したデー タセット をAtomic file形式に変換 ARTIFACT_PATH = 'artifact/train_data/' # 前のステップで保存したデータセットの保存先 input_path = ARTIFACT_PATH # Atomic fileの書き出し先 output_path = os.path.join(ARTIFACT_PATH, 'train_dataset' ) i_o_args = [input_path, output_path] # 前のステップで作成したI/Oクラスにinput,outputの情報を渡す datasets = TrainDataset(*i_o_args) # DatasetをAtomic fileに変換 datasets.convert_inter() datasets.convert_item() datasets.convert_user() 3. モデルの学習 モデルを学習する関数をノートブック上に定義 パラメータチューニングの各種設定については、コメントで記述したページに詳しく書いてあります。 from recbole.quick_start import objective_function, run_recbole from recbole.trainer import HyperTuning # (再掲) 事前に準備したRecBoleのcofig fileへのパス TRAIN_YAML_PATH = 'config/train.yaml' # 探索したいハイパーパラメータと探索範囲を記述したファイルへのパス HYPER_PARAMS_PATH = 'config/model.hyper' def train_model (model_name: str , config_file_list: str = TRAIN_YAML_PATH, params_file: str = HYPER_PARAMS_PATH) -> None : # ハイパーパラメータチューニングの条件を設定 # 参考: https://recbole.io/docs/user_guide/usage/parameter_tuning.html hp = HyperTuning( objective_function=objective_function, algo= 'bayes' , early_stop= 3 , max_evals= 15 , params_file=params_file, fixed_config_file_list=config_file_list, ) # チューニングを実行 hp.run() # print best parameters print ( 'best params: ' , hp.best_params) # print best result print ( 'best result: ' ) print (hp.params2result[hp.params2str(hp.best_params)]) # bestなパラメータを取得 parameter_dict = { 'train_neg_sample_args' : None , } | hp.best_params # bestなパラメータでモデルを学習 run_recbole( model=model_name, dataset= 'train_dataset' , config_file_list=config_file_list, config_dict=parameter_dict, ) 探索したいハイパーパラメータとその探索範囲を model.hyper というファイルに記述して用意 # config/model.hyper (使用アルゴリズムがFMの場合) learning_rate choice [0.1, 0.05, 0.01] embedding_size choice [10, 16, 32] 定義した学習用の関数に試したい アルゴリズム 名を指定して、学習を実行すると train.yaml に記述した checkpoint_dir 配下に学習済のモデルが保存されます。 # FM train_model('FM') # NFM train_model('NFM') # AFM train_model('AFM') # DeepFM train_model('DeepFM') 4. 学習したモデルの検証 学習したモデルでテストデータに対する予測値を計算し、その評価指標を算出する関数をノートブック上に定義 import torch from recbole.data.interaction import Interaction from recbole.quick_start import load_data_and_model def eval_model ( model_file_name: str , model_saved_dir: str = train_config[ 'checkpoint_dir' ], target: str = train_config[ 'LABEL_FIELD' ], user_columns: list = USER_COLUMNS, item_columns: list = ITEM_COLUMNS, token_columns: list = TOKEN_COLUMNS, ): # 学習したモデルとテストデータを読み込み _, model, _, _, _, test_data = load_data_and_model(model_file=os.path.join(model_saved_dir, model_file_name)) columns = user_columns + item_columns + [target] # テストデータをモデルが予測出来る形式に変換 interactions = {} test_df = pd.DataFrame([]) for c in columns: test_features = torch.tensor([]) for data in test_data: test_features = torch.cat([test_features, data[ 0 ][c]]) if c in token_columns: test_features = test_features.to(torch.int) interactions[c] = test_features if c in [ 'user_id' ] + [target]: test_df[c] = test_features test_interaction_input = Interaction(interactions) # テストデータに対する予測結果を作成 model.eval() with torch.no_grad(): test_result = model.predict(test_interaction_input.to(model.device)) test_df[ 'pred' ] = test_result # テストデータに対するランキングメトリクス、AUC, Loglossを算出する関数を実行 (今回は実装は割愛) # 現状のRecBoleの仕様だとmode: labeledで学習した場合、ランキングメトリクスを指定できないので、自前で計算する必要があります return test_df 定義した検証用の関数に学習済のモデルのファイル名を入れて実行することで、テストデータに対する予測結果とメトリクスが計算されます。 # FM test_df = eval_model( 'FM-%m-%d-%Y_%H-%M-%S.pth' ) # NFM test_df = eval_model( 'NFM-%m-%d-%Y_%H-%M-%S.pth' ) # AFM test_df = eval_model( 'AFM-%m-%d-%Y_%H-%M-%S.pth' ) # DeepFM test_df = eval_model( 'DeepFM-%m-%d-%Y_%H-%M-%S.pth' ) 作成された test_df にはランキングメトリクスが計算出来るようにuser_id、真値、予測値が書き込まれています。   user_id target pred 1 1 1 0.9 2 1 1 0.7 3 1 0 0.3 4 1 0 0.1 test_df を元にテストデータに対するメトリクスを計算することで、 アルゴリズム 間の比較検証が出来ます。   ROC -AUC Logloss Recall@20 MAP@20 MRR@20 FM 0.865 0.105 0.491 0.489 0.577 NFM 0.843 0.110 0.502 0.470 0.540 AFM 0.865 0.103 0.507 0.487 0.568 DeepFM 0.862 0.101 0.523 0.522 0.598 おわりに 今回は、多様なレコメンド アルゴリズム を検証できるRecBoleを活用した実験の手順について紹介しました。 この記事が、レコメンド アルゴリズム 構築に関わる方々の助けに少しでもなれたら嬉しいです! We’re Hiring! タイミーのデータ統括部では、ともに働くメンバーを募集しています!! 現在募集中のポジションは こちら です! 「話を聞きたい」と思われた方は、是非一度 カジュアル面談 でお話ししましょう! *1 : PyPI : https://pypi.org/project/recbole/ *2 : 例: CTR予測など *3 : RecBoleが学習に用いる データ形式
この記事は Timee Advent Calendar 2023 の7日目の記事です。 qiita.com こんにちは、データ統括部データサイエンス(以下DS)グループ所属の小栗です。 本記事では、メンバーの相互理解を深めるためにDSグループで取り組んでいる施策を紹介します。 そもそもの課題感 以下の要素により、DSメンバー間の相互理解が今後難しくなりそう…という課題感が当時あり、諸々の施策をスタートさせました。 フルリモート前提の働き方をしている チームメンバーの数がすごい勢いで増えてきた(1年で2.5倍に) メンバーがそれぞれ担当する部署横断PJがいくつも並行に走っており、逆にチーム内での接点が少なくなってきた メンバーの相互理解を深めるためにやっていること データ統括部やDSグループで取り組んでいる取り組みは他にもたくさんありますが、今回は以下の2つに絞って紹介します。 スキルマップ共有会 ストレングスファインダー共有会 スキルマップ共有会 スキルマップとは、業務に関係あるハードスキルとそれに対する各メンバーの習熟度を可視化するツールです。 スキルマップの作成・共有をする目的は主に2つあります。 各メンバーが持つハードスキルを把握し、相互理解に繋げる 業務に関係あるスキルと、そのスキルに自信がある人を把握できるようにする 具体的には、下図のようなスキルマップを スプレッドシート で簡単に作成しました。 スキルマップの一例 社内や社外のスキルマップを参考にしつつも、タイミーのDSグループに強く関係するスキルや技術を独自にピックアップしています。 具体的には、「データサイエンス」「データエンジニアリング」「ビジネス」「アカデミック ドメイン 」「業界 ドメイン 」の軸でそれぞれ10~20個ほど要素(≒ スキル)を選定し、軸ごとにスキルマップを作成しています。 また、スキルの習熟度とは別に、「今後伸ばしたい」スキルも可視化するようにしています。 工夫した点(というか難しい点)として、各スキルの習熟度に対して厳密な基準を設けない形にしています。 厳密な基準を設けるのは難しいこと、そして、相互理解が目的なのでメンバーそれぞれの各スキルに対する「自信度」がザックリわかればいい、といった理由から、現在の形で運用しています。 定期的に各メンバーはスキルマップへの記入を行います。 その後に共有会を開催し、1人ずつ自分のスキルを発表して、それに対する感想や質問をする、という形で運用しています。 共有会で各メンバーの 保有 スキルや今後伸ばしたいスキルが分かるため、それを通して相互理解が深まり仕事がしやすくなる、という想いで運用しています。 ストレングスファインダー共有会 ストレングスファインダー は、米国のギャラップ社が開発した「強みの診断」ツールです。 WEB上で診断を受けると、34の資質の中から自分の強みや資質を知ることができます。 スキルマップはハードスキルに着目していましたが、ストレングスファインダーではソフトスキルに焦点が当たっており、DSグループでは棲み分けをする形でどちらも運用しています。 新メンバーがオンボーディングの過程で診断を受けるフローになっており、メンバーが増えるごとに都度共有会を開いています。 共有会では、1人ずつ自分の強みを発表して、それに対する感想や質問をする、という形で賑やかに開催しています。 また、診断結果を盲目的に信じて決めつけたコミュニケーションをするのではなく「この診断結果は的確/そうでもない」といった会話も挟むなど、あくまで診断結果を踏み台にして生まれる会話によって相互理解を深めています。 ストレングスファインダーの診断結果を通して、普段の業務では把握できない各人の「強み」や「考え」がわかるのが魅力だと思っています。 ※データ統括部BIチームのyuzukaさんが執筆した ストレングスファインダーに関するアドベントカレンダー記事 もあるため、そちらもぜひご一読ください! おわりに この記事では、DSグループが実施しているメンバー相互理解のための取り組みを紹介しました。 データ統括部およびDSグループは今後もメンバーを増員する予定なので、施策をさらにアップデートしていきたいと考えています! We’re Hiring! タイミーのデータ統括部では、ともに働くメンバーを募集しています!! 現在募集中のポジションは こちら です! 「話を聞きたい」と思われた方は、是非一度 カジュアル面談 でお話ししましょう!
この記事はTimee Advent Calendar 2023シリーズ 2の5日目の記事です。 はじめに DREグループの石井です。 今回はDREグループの管理するデータ基盤に関するインフラのterraformのテスト環境の話をしようと思います。 導入前の課題感 我々のチームではデータ基盤として複数の GCP Projectを管理していますが、その全てをterraformで管理しています。 この時点でGithubActionsによる自動テスト(validate, plan) 及び 自動デプロイは導入されていたため、レビューさえ通れば誰でもインフラの変更を反映できる状態になっています。 しかし、この時点でよく起こっていた問題として以下のようなものがありました。 validationが実装されていないリソースの 命名規則 などでデプロイ時に落ちる 実際にapplyして試してみたいけど、デプロイ先が本番しかない そこでPRごとに GCP Projectを作成しその中でapplyを実際に試せる仕組みを実装して、しばらく運用してみたので実装からその所感までをまとめてみようと思います。 やったこと PR時にテスト環境が作成するために、ざっくりいうと以下のようなことをやっていますので、それぞれ詳細に記載しようと思います。 リポジトリ 構成の変更 CIで実際にapplyされるときのリソース名の調整 GithubActionsの実装 リポジトリ 構成の変更 元々、1つの Github Repositoryで全ての GCP Projectを取り扱っていたのですが、元々は以下のような構造でした。 (必要なところのみ抜粋) envs/ GCP_ProjectA/ 各種terraform .. GCP_ProjectB/ modules/ module_A/ module_B/ global.tfvars ... ユーザの管理等 envs配下にproject単位で切られており、modulesは共通の部品だけ切り出しておくという構成です。 これを以下の様に変更しています。 envs/ GCP_ProjectA/ environment/ prod/ main.tf backend.tf ...            test/ main.tf backend.tf modules/ module_X/ 各種terraform .. GCP_ProjectB/ modules/ module_A/ module_B/ global.tfvars ... ユーザの管理等 プロジェクトを跨いでグローバルに使用していたモジュールはそのままとして、モジュール化されていなかったプロジェクト固有のterraformファイルをモジュールとしてまとめています。 そして、main.tf内でモジュールを呼び出すという一般的なモジュール構成に似た形になっています。 リソース名の修正 リソース名もvalidation対象ではあると思うので本来はそのまま適用していきたいのですが、GCSのようなグローバルに一意にしないといけないリソースはこのまま実行するとテスト環境を壊すのと本番環境を作るときのラグ等でリソース名が利用できなかったりして困るケースがあります。 そのため gcs_suffix というvariableを用意しておいて、module側で例えば以下のようにしています。 resource " google_storage_bucket " " gcs " { name = " test${var.gcs_suffix} " .. } default値を ""(空文字) としておくことで、本番環境には影響を与えないようにしています。 Github Actionsの実装 それでは上で整理したモジュールを使ってテスト環境を生成する部分の話に移ります。 基本的にはPJを作るテンプレートを別途用意しておき、それをコピーしてきてapplyしてプロジェクトを作成、その後、environment/test内をapplyする、という流れになっています。 project.tf (Projectの作成) resource " google_project " " test-environment " { billing_account = var.BILLING_ACCOUNT_ID name = local.project_id project_id = local.project_id org_id = var.ORGANIZATION_ID } resource " google_billing_budget " " budget " { depends_on = [ google_project.test-environment ] provider = google-beta billing_account = var.BILLING_ACCOUNT_ID # 消し忘れ対策にバジェットを指定 amount { specified_amount { currency_code = " JPY " units = 10000 } } budget_filter { projects = [ " projects/${google_project.test-environment.number} " ] } threshold_rules { threshold_percent = 0.5 ... } services.tf (サービスの有効化) resource " google_project_service " " service " { depends_on = [ google_project.test-environment ] for_each = local.services project = local.project_id service = each.value disable_dependent_services = true } (variable等は割愛します) これをapplyした後に実際のtest配下をapplyするのですが、backend設定だけ差し替えないといけないため、以下のようなテンプレートを用意して書き換えています。 backedn.tf.template terraform { backend \ " gcs\ " { bucket = \ " terraform-backend-bucket-name\ " prefix = \ " ci_projects/${PROJECT_ID}/projects.tfstate\ " } } これらを用いて、以下のようなActionsになりました。なお、PJがかなり多いため実際によく変更されるプロジェクトのみを対象としたかったのでそうなるようにしています。 name: terraform-test-apply on : pull_request env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GOOGLE_BACKEND_CREDENTIALS: ${{ secrets.GOOGLE_BACKEND_CREDENTIALS }} GOOGLE_CLOUD_KEYFILE_JSON: ${{ secrets.TIMEE_CORE__GOOGLE_CREDENTIALS }} jobs: set -matrix: runs- on : ubuntu-latest outputs: target_project: ${{ steps.get-diff.outputs.value }} steps: - uses: actions/checkout@v3 - name: Fetch changes run: git fetch origin ${{ github.base_ref }} - name: id: get-diff run: | diff=$( echo " $(git diff --name-only origin/master..HEAD | \ cut -d'/' -f2 | \ grep -e 'Project1' -e 'Project2' -e 'Project3\' | \ jq -R . | jq -s . | jq -c '.|unique') " ) echo $diff echo " value=${diff} " >> $GITHUB_OUTPUT apply: needs: set -matrix if : ${{ needs. set -matrix.outputs.target_project != ' [] ' }} strategy: fail-fast: false matrix: PROJECT_NAME: ${{fromJson(needs. set -matrix.outputs.target_project)}} runs- on : ubuntu-latest env: PROJECT_ID: " tf-test-${{ matrix.PROJECT_NAME }}-${{ github.event.number }} " PR_NUMBER: ${{ github.event. number }} steps: - uses: actions/checkout@v3 - name: Setup Terraform uses: hashicorp/setup-terraform@v2 - name: create project tf and apply run: | pushd tests/project eval " echo \ " $(cat ../templates/backend.tf.template )\ "" > backend.tf terraform init - lock = true - lock -timeout=60s terraform apply -var= " PROJECT_ID=${PROJECT_ID} " -auto-approve -var- file =../../global.tfvars - lock = true - lock -timeout=60s project_number=$(terraform output google_project_number | head -n 2 | tail -1 ) echo " project_number=$project_number " >> $GITHUB_ENV popd - name: apply `iam/` run: | pushd envs/${{ matrix.PROJECT_NAME }}/environment/test/ terraform init -backend-config= " prefix=${{ matrix.PROJECT_NAME }}/${PROJECT_ID}/test.tfstate " terraform apply -var= " project=${PROJECT_ID} " -auto-approve -var- file =../../../../../global.tfvars - lock = true - lock -timeout=60s -parallelism= 20 - name: Terraform apply Link uses: actions/github-script@v6 with : result-encoding: string script: | github.rest.issues.createComment({ issue_number: context.issue. number , owner: context.repo.owner, repo: context.repo.repo, body: " ### Test Apply of `${{ matrix.PROJECT_NAME }}` :rocket: " + " \n " + " Link: https://console.cloud.google.com/welcome?project= " + process.env.PROJECT_ID }); やってみた感想と課題 正直1PRごとに1GCP Projectを立てるのはやりすぎかなと思っていましたが、実際立っていると確認はしやすく、かつ他の影響も受けないため特に新しい機能を開発するようなタイミングでは大変良かったように思います。 ただ、現実問題としてapplyにかかる時間がだいぶ長いという問題はあり、環境によっては4,50分かかっていたこともありました。 検証する必要性の薄いリソースを対象外とするなど色々改善しましたが、それでも軽微な変更をするのにもこれを回さないといけないというのはやりすぎでは?という側面も正直あるかなと思っています。 このあたりは程度問題な気もするので、今後も見極めていければとは思っています。  個人的には初めて行う設定などでterraformの書き方にやや自信がないものをある程度自信を持ってレビューを出せるようになったことに最も価値を感じているところではあります。 We’re Hiring DREグループではまだまだやっていきたいことがたくさんあるのですが、まだまだ手が足りておら ず、ともに働くメンバーを募集しています!! データエンジニアのポジション データに係る他のポジションやプロダクト開発などのポジションも絶賛募集中なので こちら からご覧ください
こんにちは、CTO室グループでQA スペシャ リストを担っている依光です。 今年を振り返ってという視点から、施策として動き始めた「障害対応をFactから改善する取り組み」について紹介させてください。 今までの取り組みと課題 タイミーのプロダクト部ではFour Keysを活用して改善サイクルに取り組んでおり、 プロダクトの品質を改善するという側面から「変更失敗率」と「サービス復元時間」を 計測しています。 この「サービス復元時間」を短縮するに当たり、障害を時系列にまとめて事後検証として 振り返るポストモーテムだけでは、改善するポイントを客観的に判断することが難しい という課題がありました。 取り入れた施策 障害発生の時間を短縮するために発生時間の内訳を計測して、客観的に判断するようにしました。 まず計測する際に時間を分解する切り口は、O'Reilly社から出版されている「Seeking SRE(SREの探求)」に記載されているプロセスを参考に収集するようにしました。 図の 参照元 : https://www.oreilly.com/library/view/seeking-sre/9781491978856/ch04.html TTD:検出時間 TTE:エンゲージ時間( エス カレーションプロセスの時間) TTF:修正時間 TTM:軽減時間(障害発生から対応が完了するまでの時間) 続いて客観的に判断するために、各障害のレベルごとにTTD、TTE、TTF、TTMの目標指標を定義しました。 表:障害レベルと各プロセスの目標指標の例 施策を取り入れた効果 ポストモーテムの実施タイミングで、発生した障害のレベルに応じて TTD、TTE、TTF、TTMが目標指標内に収まっているかを確認する ステップが追加されました。 そして未達の指標に対して改善案を深ぼるアプローチができるようになり 障害発生時間の短縮に結びつく改善に繋がりやすくなりました。 指標例:TTD(検出時間)が未達だった 改善例:メトリクスの追加とアラートについて検討する 今後の取り組みに向けて 今回紹介した施策は、時系列にまとめて事後検証を行うポストモーテムだけでは 見つけられなかった改善ポイントが、障害発生時間の内訳時間というFactを基に、 客観的に把握して品質改善ができるようになった1例だと考えています。 今回の事例だけでなく、今後もタイミーのプロダクト本部ではFactを基にした 品質の維持、向上の取り組みを大切にしていきたいと考えています。 私達の取り組みにご興味がありましたら、 情報交換など気軽にご連絡下さい。 最後まで読んでいただき、ありがとうございました。
こちらは Timee Advent Calendar 2023 シリーズ1の5日目の記事になります。 昨日は @redshoga による Vercel REST APIを用いたステージング環境反映botについて で明日は @yama_sitter による フロントエンドアプリケーションの認知負荷とテスタビリティに立ち向かう です。 タイミーでバックエンドエンジニアをしている id:euglena1215 です。 タイミーはユーザー向け・企業向け・社内向けの機能を1つの Rails アプリケーション上で動かしています。 10/5に Rails 7.1 がリリースされ、タイミーも11/1に 7.1.1 に上げることができました。現在は Rails 7.1.2 が本番で元気に動いています。 Rails 7.1.1 へのアップグレードは比較的スムーズに行うことができたものの、アップグレードのプロセスにはまだ改善の余地があると感じました。今回はどこに改善の余地があると思ったのか、具体的な改善の取り組みについて紹介したいと思います。 背景・課題 指標その1:Code Line Coverage Code Line Coverage とは 指標その2:Endpoint Coverage 改善その1:index actionのテストめっちゃ書く 改善その2:使われていない batch_action の削除 まとめ 背景・課題 タイミーでは、 Rails アップグレードの動作確認は手動での動作確認をせずとも自動テストで動作を担保して問題ないという合意が得られています。これはテストの網羅性が高く(Code Line Coverage が 91%)十分に動作が担保できているだろうという前提があるためです。 しかしこのルールにもいくつかの例外があります。例えば、暗号化 アルゴリズム の変更やキャッシュフォーマットの変更、 Cookie の属性変更など普段書くアプリケーションロジックの自動テストでは検出が難しいと考えられるものです。これらの例外は毎アップグレードで確認すべき箇所が異なり自動テストで検出するのは現実的ではないと考えているため、自動テスト以外の方法で動作を担保しています。 一方、他の例外として「社内向け管理画面は手動でのチェックを行う」というものがあります。これは社内向け管理画面はテストコードの網羅性が低く、自動テストを信頼できないというのが理由です。理由としてはもっともだと思いますが、ユーザー向けの機能は手動でのチェックをしなくていいのに、社内向けの機能は手動でのチェックが必要なのはチグハグさを感じました。 今回はこの 社内向け管理画面はテストの網羅性が低く自動テストを信用できないため、 Rails アップグレードの手間が増えている ことが課題だと捉えました。 指標その1:Code Line Coverage テスト カバレッジ が低いのならテストを書けばいいじゃないということで、社内向け管理画面 ( /admin/* )の Code Line Coverage を指標としテストを書き始めました。 Code Line Coverage とは 「Code Line Coverage」は、 ソフトウェアテスト の際に使用される指標です。その目的は、自動テストによって実行された ソースコード の行数の割合を測定し、どの程度のコードがテストされているかを把握することにあります。計算方法は、テストで実行されたコード行数を全コード行数で割り、それを100倍してパーセンテージで表します。この指標はテストの カバレッジ を 定量 的に評価するために使われます。 タイミーの Rails アプリケーションでは codecov を使って以下のようにテストで実行されたコード、実行されていないコードを可視化しています。 この時点での app/admin/* ディレクト リの Code Line Coverage は 75% でした。 前提として、タイミーでは社内向け管理画面を実装するために ActiveAdmin gem を利用しています。 ActiveAdmin gem を使うこと自体には社内でも賛否両論ありますが、大いなるデメリットもあれば大いなるメリットもあるということで使い続けています(この辺りの話に興味があればぜひ話を聞きに来てください。カジュアル面談は こちら )。 問題とは、ActiveAdmin を使って管理画面を実装した場合 Code Line Coverage と体感のテスト カバレッジ が一致しないことです。 代表例を挙げます。下記は User モデルに対応した管理画面を実装するコードです。 この2行を書くだけでいくつのエンドポイントが定義されるかを確認してみましょう。 # app/admin/users.rb ActiveAdmin .register User do end 下記は上記2行の実装によって定義されたエンドポイントの一覧です。2行書くだけで9つものエンドポイントが定義されています。 root@ba8730884fed:/usr/src/app# bundle exec rails routes | grep admin/users batch_action_admin_users POST /admin/users/batch_action(.:format) admin/users#batch_action admin_users GET /admin/users(.:format) admin/users#index POST /admin/users(.:format) admin/users#create new_admin_user GET /admin/users/new(.:format) admin/users#new edit_admin_user GET /admin/users/:id/edit(.:format) admin/users#edit admin_user GET /admin/users/:id(.:format) admin/users#show PATCH /admin/users/:id(.:format) admin/users#update PUT /admin/users/:id(.:format) admin/users#update DELETE /admin/users/:id(.:format) admin/users#destroy また、ActiveAdmin は動的にルーティングを生成するため、 /app/admin/* ディレクト リ以下のファイルは Rails 起動時に読み込まれ評価されます。そのため、 app/admin/users.rb に対応するテストを1つも書かなくても app/admin/users.rb の Code Line Coverage は 100% になります。 もちろんこれは極端な例で、普段は一覧の要素を変更するなど実際にアクセスしないとカバーできないコードが生まれるため Code Line Coverage が 100% になるケースは少ないです。だとしても対応するテストがないのに 50%以上のファイルがいくつかあったりと全体的に高く出過ぎているように感じました。 ActiveAdmin は高機能な DSL によって数行でいくつもの画面を生成できます。そのため、コード量の機能量が比例しません。よって、ActiveAdmin による管理画面の実装において Code Line Coverage は指標として不適切だろうと判断しました。 指標その2:Endpoint Coverage タイミーでは一番外側のテストとして system spec は書かず request spec を書いています。エンドポイントに対応したテストが1つ以上あれば一定動作は保証されているだろうと考え、 /admin/* のエンドポイントに対して request spec が何割カバーできているかを指標として改善をしていくことにしました。 具体例 GET /admin/users テスト🙆‍♂️ GET /admin/users/:id テスト🙆‍♂️ POST /admin/users テスト🙅‍♂️ ↓ ↓ ↓ ↓ ↓ ↓ テストされたルート数: 2 テストされていないルート数: 1 全ルート数: 3 カバレッジ: 66.67% ここでは Endpoint Coverage と呼称することにします。(正式名称あれば教えてください。訂正します。) なるべくシンプルな方法で Endpoint Coverage を集計することにしました。ステップは以下です。 rails routes 相当の情報を取得 spec/requests/* ファイルを読み込みルーティングに対応する describe 句を抜き出す(describe 句があればテストケースは1つ以上あるだろうと判断) 1.と2.で得られたデータを組み合わせて、テストされているエンドポイント・テストされていないエンドポイントを分類し、割合を算出する 集計用の Rake タスクは以下になります。興味があれば見てみてください。 エンドポイントごとのテストカバレッジを計測するための rake タスク · GitHub 上記の Rake タスクをタイミーの Rails アプリケーションで実行すると以下の結果が得られました。 root@084b7fc12fd0:/usr/src/app# bundle exec rake endpoint_coverage:collect_for_admin テストされたルート数: 165 テストされていないルート数: 367 全ルート数: 504 カバレッジ: 32.74% root@084b7fc12fd0:/usr/src/app# bundle exec rake endpoint_coverage:collect テストされたルート数: 457 テストされていないルート数: 445 全ルート数: 827 カバレッジ: 55.26% 確かに admin は カバレッジ が 32.74% と高くないことが分かります。体感としてもそのくらいです。 次に全体の カバレッジ は 55.26% でした。こちらの結果には admin も含まれているため、admin を抜くと 78.9% です。 上記の結果より、admin とそれ以外とでは2倍以上の開きがあることが分かりました。これならテストの網羅性が低いという判断も納得できます。ということで、Endpoint Coverage を指標として用い社内向け管理画面のテストの網羅性を高めていくことにしました。 また、「社内管理画面の手動での動作確認では機能の60%もカバーできていないだろう」ということで Endpoint Coverage は 60% を目標として進めていくことにしました。 改善その1:index actionのテストめっちゃ書く Endpoint Coverage の集計 Rake タスクの副産物として、テストが書かれていないエンドポイント一覧を入手しました。これを元にテストの拡充を進めていきたいと思います。 まずは、index action に対応する request spec を書いていくことにしました。テストケースとしては 200 を返すことを検証します。 この判断をした理由は以下の通りです。 index action は一覧を取得するためのエンドポイントであり、多くの利用者は一覧画面を起点にして操作を行うため比較的重要度が高い index action の 200 を返すテストはある程度 機械的 に追加できるため、テストを追加するのが楽 spec ファイルが元々あるのとないのとではテストを追加する際の 心理的 ハードルが異なるため、とりあえずファイルだけでも作っておくことで他のエンドポイントのテスト追加がされやすくなるのではという期待 というわけでテストを書き始めていると嬉しい誤算に気付きました。それは GitHub Copilot がかなり補完してくれることです。 # frozen_string_literal: true require ' rails_helper ' RSpec .describe ' Admin::User ' do describe ' GET /admin/users ' do # ここまで書くと... end end # frozen_string_literal: true require ' rails_helper ' RSpec .describe ' Admin::User ' do describe ' GET /admin/users ' do # 以下を全て補完してくれる! subject { get admin_users_path } context ' 正常系 ' do before do create_list( :user , 2 ) end it ' 200を返す ' do subject expect(response).to have_http_status( :ok ) end end end end テストが書かれていないエンドポイント一覧は手元にあるため、ほとんどエンドポイントをコピペするだけで index action に対応する request spec を増やしていくことができました。 その結果、以下のように 32.74% → 41.47% まで カバレッジ を伸ばすことができました 🎉 root@084b7fc12fd0:/usr/src/app# bundle exec rake endpoint_coverage:collect_for_admin テストされたルート数: 209 テストされていないルート数: 326 全ルート数: 504 カバレッジ: 41.47% 改善その2:使われていない batch_action の削除 テストが書かれていないエンドポイント一覧を眺めていると、多くのリソースに対して batch_action エンドポイントが生えていることが分かりました。これは一覧画面で各リソースにチェックを入れて一括で削除するといった処理を行うために ActiveAdmin が用意しているものになります。 Active Admin | The administration framework for Ruby on Rails batch_action をデフォルトで有効にするかどうかは設定で変更可能なのですが、タイミーでは有効になっているようでした。 「一括操作ってそんなに行うことある?」と思い過去1年間のログを確認したところ、batch_action が使われているリソースは1つしかありませんでした。そのため、全体では無効化し使われているリソースにのみ batch_action を有効化しました。 これまで定義されていた batch_action は気付かず定義されていたのか、開発者による善意のものだったのかは判断できませんが Code Line Coverage では気付くことが難しく Endpoint Coverage を見ていたからこそ気付けたものかなと思っています。 その結果、以下のように 41.47% → 48.98% まで カバレッジ を伸ばすことができました 🎉 root@084b7fc12fd0:/usr/src/app# bundle exec rake endpoint_coverage:collect_for_admin テストされたルート数: 217 テストされていないルート数: 257 全ルート数: 443 カバレッジ: 48.98% 目標としていた Endpoint Coverage 60% にはまだ届いていませんが、元々が 32.74% だったことを考えるとかなり近づいたのではと思っています。 まとめ Rails アップグレードの手間を減らすために社内向け管理画面のテストを拡充させようと考え、Endpoint Coverage という指標を定義しました。Endpoint Coverage を改善するためにいくつかの改善を行い、約30%から約50%と目標の60%に近づけることができました。Endpoint Coverage を定義し、テストされていないエンドポイント一覧がわかったことで様々な改善ア イデア が思いついたように感じます。 これからも Endpoint Coverage を高めていき Rails 7.2 アップグレードでは手動での動作確認が必要なくなるよう頑張っていこうと思います。
こんにちは、データ統括部でデータアナリストをしているyuzukaです。 今回は、データ統括部のアナリストを対象に実施した「ストレングスファインダー共有会」について紹介します。 ストレングスファインダーとは? ストレングスファインダー とは、米国のギャラップ社が開発した「強みの診断」ツールです。 WEB上で177個の質問に答えると、34の資質の中から自分の強みや資質を知ることができます。 診断は有料ですが、弊社では会社の経費で受けられます。(ありがたい・・・!) 私の強みTOP10はこんな感じでした。 ストレングスファインダーに着目した背景 個人の強みに着目するコンセプト 職場での性格診断に適したツールとして、MBTIやDISCなど有名どころはいくつかありますが、中でもストレングスファインダーは個人の「強み」にフォーカスしているところが良いと感じました。 ただ相互理解を深めるだけでなく、それぞれの強みを仕事に活かしていこう、という前向きな気持ちになることができます。 実際にストレングスファインダー共有会で利用した資料 実際に取り組んだこと 事前準備 ストレングスファインダー共有会の開催にあたり、準備した内容は以下の通りです。 ストレングスファインダーのアクセスコードを、人数分まとめて購入して配布する 参加者にはあらかじめ診断を受けてもらい、診断結果とひとこと所感を記入しておいてもらう 参加者に負担がかからないかつ、当日の進行がスムーズになるようにロジ周りを意識しました。 ストレングスファインダー共有会の内容 オンラインで集まり、和気あいあいとした雰囲気で強みの共有会を行いました。 ルーレットでランダムに指名して、1人ずつ自分の強みを発表 事前に記入してもらった診断結果と所感を見ながら、4-5分話してもらう みんなでわいわい、感想や質問を言い合う これを人数分繰り返す形式で実施しました。 当日は想定していた以上に盛り上がり、時間があっという間に過ぎ去りました。 「新しいことを始めるのが得意」「学習欲が強い」「目標に向けて、人一倍努力できる」「ゲームのオリジナルルールを考案するのが得意」など、参加者それぞれのユニークな強みを知ることができました。 おわりに ストレングスファインダー共有会の開催からしばらく経ちますが、 通常業務のコミュニケーションの中でも、お互いの強みに言及する機会が増えた ように思います。 データ統括部は今後も積極的にメンバーを増やしていく予定なので、ストレングスファインダー共有会も、メンバーが増えたタイミングでの定期開催を考えています。 We’re Hiring! タイミーのデータ統括部では、私たちとともに働くメンバーを募集しています。 product-recruit.timee.co.jp
この記事は "Timee Advent Calendar 2023" の2日目の記事です。 qiita.com こんにちは、株式会社タイミーの土川 ( @tvtg_24 ) です。 先日、マネー フォワ ードさんとアナリティクスエンジニアリング周りの合同勉強会を開催しました! 自分が所属するデータ統括部では初めての試みだったんですが、とても良い会になったと思うので少しこの記事で紹介できたらと思います! きっかけ マネー フォワ ードの木宮さん ( @yuu_kimy ) と自分は定期的にご飯に行っていて、いつもお互いのデータ関連の話をたくさんさせていただいてました! そんな中、もっと社内のメンバー巻き込んで情報交換したいね、みたいなお話をしていたところ、ちょうどRettyさんとマネー フォワ ードさんで合同勉強会を開いたとのことでしたので、それを参考に合同勉強会を実施しました。 木宮さんには、当日の アジェンダ 作成など合同勉強会実施のための諸々準備をしていただいて、とても感謝してます! 当日の様子 当日の アジェンダ アジェンダ は以下のようになっていて、発表は各社2つずつ行い、最後に時間が許す限りフリーディスカッションでdbtについて話しました。 会自体はオンラインで行い、お互いの会社から計10名以上が参加する賑やかな会になりました! アジェンダ 発表振り返り 発表はタイミー側から 大越さんと、okodoon ( @miburo_data ) が、それぞれ「データアナリストがdbtを触ってみた」、「dbtジョブ分割実行について色々考えた話」というテーマで発表しました。 マネフォさん側からは、奥野さん ( @RossOkuno )と木宮さんが、それぞれ「マルチテナント分析基盤について」、「dbtをAirflowで動かす道のりは続く」というテーマで発表しました。 発表のダイジェストについてはマネフォさん側の記事 ( https://note.com/yuu_kimy/n/n236c5d5047ad ) で紹介されているので、詳しくはそちらをご覧ください。 マネフォさんとタイミーは使っているデータ基盤サービスは似ているんですが、プロダクトの数や、事業規模が違ったりするため、複数組織でのデータ分析環境の提供についてのノウハウや、苦労したところなどがたくさん聞けて、すごく勉強になりました。また、Cosmosといったタイミーが使っていない新しめの python パッケージについての利用感が聞けたのはとてもよかったです。タイミーでも今後の参考にしていきたいと思いました。 dbt活用に関するフリーディスカッションについて フリーディスカッション 会の最後には、dbt活用に関するフリーディスカッションを1時間ほどしました。 発表自体で消化できなかった質問や、dbt Coalesce で話題になっていたdbt Cloud CLI などを使った、dbt Core, dbt Cloudの使い分けについてなど、様々な議論をしました。 また普段あまりオープンな発表では公開しづらい、各々のデータ モデリング の詳細などについて話せたのはとてもよかったと思います。 終わりに 今回は、初めての合同勉強会でしたが、とても楽しく、有意義な時間になりました。 会社ぐるみで一つのテーマに対して、深く議論する機会は普段なかなかないのと、やはりオープンな場より、一歩踏み込んだ情報をもとに議論ができることが個人的にとても良いなと思いました。 また、より良いデータ分析環境を提供するという同じ目標を掲げて試行錯誤しているマネフォさんをみて、タイミーも追いつけるように頑張らないと!という気持ちになりました! これからも合同勉強会のような機会は増やしていけたらなと思っているので、興味のある会社さんはぜひX (旧Tiwtter) でお声かけください! 採用関連 タイミーは絶賛採用中です!ぜひお力を貸してください! hrmos.co
はじめに こんにちは、マッチング領域でバックエンドエンジニアをしているぽこひで (  @pokohide  ) です。 タイミーの アドベントカレンダー 2日目の記事です。 今回は、タイミーのプロダクト組織で毎週開催している 技術的な雑談を行うテック トーク の紹介をします。なぜ開催しようと考えたか、どのように運用をしているかなどをお話しします。 はじめに 開催の背景 毎週ゆるく開催するテックトークについて テックトークの仕組み化 会の説明や目的の共有 WINの共有 ポストモーテムの学び共有 雑談タイム やってみて さいごに 開催の背景 タイミーのプロダクト組織では、働き方の柔軟性を担保する観点などから フルリモートという働き方を選択 しています。また、タイミーではチーム トポロジー を採用しており、それに沿ってチーム構成などを考えています。 チーム トポロジー の変遷や取り組みについてはCTOとCPO(発表当時はVPoT)が過去に発表を行っているので詳しく知りたい方はこちらをご覧ください。 チームトポロジー Vol. 2 「組織をチームトポロジーで振り返るメリット」タイミー 亀田 彗 | ITエンジニア向けのトレンド情報 組織をスケールさせるための Four Keys とチームトポロジー - Speaker Deck チーム トポロジー によって、チームが 疎結合 になることでコミュニケーションの複雑性が抑えられたりチームの柔軟性が増すなど利点はありますが、インフォーマルコミュニケーション(業務外の日常的な会話や雑談)が減り仲間意識が薄れる可能性もあります。さらに、フルリモートはこの流れを助長する可能性があります。 疎結合 性を確保しつつも適度なコミュニケーションや協力を促進するバランスが重要と考え、テック トーク という会を開催 *1 しようと考えました。 2023年9月に発売された「GitLabに学ぶ 世界最先端のリモート組織のつくりかたドキュメントの活用でオフィスなしでも最大の成果を出すグローバル企業のしくみ」でもリモート組織を作る観点でですが、インフォーマルコミュニケーションが従業員のパフォーマンス向上や メンタルヘルス の問題解決に重要な役割を果たすという研究結果が紹介されています。 GitLabのカルチャーは、「GitLab Value 」「仲間意識(信頼と友情)」「ワークスタイル」という3つの要素で構築されており、仲間意識を醸成するためにインフォーマルコミュニケーション(業務外の日常的な会話、雑談、何気ないやり取り)が「意図的」に設計されています。 オフィスなしのオールリモートで成長するGitLab社、世界中の2,000人をつなぐカルチャーとは|CodeZine(コードジン) から引用 と、それっぽい理由を書いていますが、他のチームで起きた技術的に面白い事やその時々の興味深いテックニュースなどをゆるく駄弁りたいという個人的な欲求が発端というのもあります。 毎週ゆるく開催するテック トーク について 2023年2月から毎週金曜日に夕方から最長1時間枠で開催している技術的な雑談を行う会です。 SlackのHuddleを利用しており、会話から抜ける時の確認は不要で入退室は自由です。 テック トーク 技術的な話題である事のみが制約で、あとは雑談でもLTの練習場でも何でも自由に使って良い時間としています。 話題は持ち寄り制で、最近の技術ネタや面白いPRなどがあれば自由に書いてもらう形式で運用しています。何もなければその場の雰囲気で会話したり、早めに会を終わらせています。 とはいえ、持ち寄るのも大変なので会の冒頭10分に、1週間にあった技術的なWINやポストモーテムの学びを共有する時間を設けています。ここからさらに深掘りたいものがあれば話題に移して話しましょうといった流れです。 テック トーク の仕組み化 テック トーク を開催するにあたって、参加者に満足してもらいたいのは勿論ですが、それ以前に 継続的に開催する事 が大切と判断し、そちらを重視しました。 そこで運用コストを最小化するためにも内容をシンプルにし、仕組み化にもこだわっています。 開催するからには気軽に参加してもらいたいのでSlackbotのリマインダーを活用しつつ、Slack Huddle上で開催しています。当初は Google Meetを使っていましたが、カレンダーに招待されていないと暗黙的でクローズドな会と誤解されると思ったからです。 時間配分や会の内訳はシンプルです。 会の説明や目的の共有 WINの共有 ポストモーテムの学び共有 雑談タイム 以上です。前半の共有は経験則的に長くとも10分程度で終わります。タイムキーパーを用意していません。あとは適当に駄弁るだけです。 会の説明や目的の共有 会によっては、初めて参加してくれる方もいるので「この会は、テックな話題である事以外は特に制約はなく、入退室も自由なゆるい会ですよ」とお伝えしています。 WINの共有 タイミーのプロダクト組織では、技術的に面白い取り組みやドキュメントの更新、エラー対応、パフォーマンス改善など文字通り WIN! と感じたものに :win: スタンプをつける文化があります。そこで、 :win: スタンプが付いた投稿を特定のチャンネルに集約して、テック トーク で触れやすくしています。 ここでは、WINを共有するだけに留め、パフォーマンス改善などその背景にあったツラミをもう少し深掘りたい場合は、後続の雑談タイムで話すようにしています。 余談ですが、 :win: というスタンプはあまり汎用的すぎる名前で他の部署がつけたWINも収集してしまい収集チャンネルがノイ ジー になっていました。そこで、 メンショングループXに所属するユーザーがスタンプYをつけた時にチャンネルZに通知するSlack BOT を作りました。 この BOT については、またどこかで紹介できればと思います。 この辺も今後はNotionに自動で転記するなどより自動化していきたいですね。 ポストモーテムの学び共有 ポストモーテムをNotionのDBで管理しているので、その週に開催されたポストモーテムだけを表示するようにフィルタリング条件を調整し、準備不要で触れられるようにしています。 ここでも時系列を振り返るのは時間をとりすぎるので、影響範囲や原因、学びのまとめのみを共有しています。 雑談タイム この時間に話す話題をDBの1レコードとして管理しています。最近は参加者も話題も増えたため、気になる人は投票してもらう制度を導入し、どの話題から触れていくかを決めています。 議事録は話題レコードの中に雑に記載しています。話題をDB管理しているのは、テック トーク での雑談からTechBlogや登壇のネタになると嬉しいな考えているからです。これも特に目的として明文化してませんが、この仕組みが機能していく事に乞うご期待です。 Rails 7.1に上げるための残作業 論理削除のつかいどころ テク ノロ ジー レーダー9月号を眺めてみる モジュラ モノリス におけるパッケージ間のDB トランザクション の扱い 技術検証を活性化するためにはどうすればいいか 例ですが、過去にはこういった話題が話されていました やってみて 2023年2月から開催し、開催回数は30回を超えました。 準備も特に必要なく、自分もただの一参加者気分で参加できているので気負わず継続できているのも良いことです。当初考えていた技術的な話題を駄弁りたい欲求も満たされ、他のチームメンバーとも交流ができています。 自分だけ述べても公平性に欠けるので、参加してくださっているみなさんのコメントも欲しかったのですが、 アドベントカレンダー の締切間近すぎて間に合いませんでした。なので、これは自分一人の勝手な妄想かもしれないですが、最近は常に6人以上は集まってくれているので客観的に見て満足はしてくれているのでしょう…(そう信じたい笑) さいごに 今回は技術的な雑談を行うテック トーク の紹介や、継続的に開催するためにどのように仕組み化しているかをお話ししました。 テック トーク の参加者は増えてきています。嬉しい悲鳴ですが、参加者が増えすぎるとこの会の主目的である 雑談 が難しくなります。それは今後の課題なので、テック トーク の フランチャイズ 化やランダムに参加者を分割して ブレイクアウト ルームのようにするなど色々試していければと思います。 また、ここで話された話題からテックブログのネタなどが生まれる事を個人的に期待しているのでそちらも乞うご期待ください。 この活動について、より詳しく知りたいと思った方はカジュアル面談受け付けておりますので、是非お話ししましょう!過去の話題や議事録は残っています笑 product-recruit.timee.co.jp *1 : 定例は増やせばいいという訳ではなく適切に開催することが大切です。過去には、技術改善の文化を根付かせるためにプロダクトミライ会議という技術的負債を扱い、どのように解消していくか話し合う会議体を設けていましたが、技術改善の文化が根付いてきたこともあり、テック トーク 開催と並行して現在はクローズしています。
この記事は Timee Advent Calendar 2023 シリーズ 1の1日目の記事です。 はじめに こんにちは、タイミーでバックエンドエンジニアをしている須貝( @sugaishun )です。昨年は弊社で アドベントカレンダー に取り組んだか覚えていないのですが、今年はなぜかいきなり3トラックで臨むということで、非常に勢いがあるなと思いました。量と勢いで攻めていくところが弊社らしいなと感じています。全て完走できると良いですね。 さて私はその中のひとつのトップバッターということで、タイミーの Rails アプリケーションについて弊社のシニアなエンジニアたちと雑談した内容を座談会風にお伝えできればと思います。事の発端は弊社Slackのバックエンドエンジニアが集まるチャンネルで「タイミーの Rails アプリケーションの健康度はどのくらいなのか?」という会話をしたことでした。その時の私の感想は「人によってけっこう基準が違うなあ」といったものでしたが、改めて話を聞いてみると自分の中で発見がありました。参加者は以下のとおりです。 参加者プロフィール 難波さん( @kyo_nanba ) シニアバックエンドエンジニア ファッションEC系スタートアップの開発責任者やメドピア株式会社の GM などを経て2022年にタイミーに入社。プラットフォームチームの立ち上げやフィーチャーチームとのコミュニケーション設計に取り組み、直近では会社の技術戦略と現場の技術的ロードマップを接続する役割を担う傍ら Rails アプリケーションの設計やリ アーキテクチャ に関わっている。 神速さん( @sinsoku_listy ) バックエンドエンジニア 2022年11月入社。CTO室に所属し、 Rails アプリケーションの開発効率を向上するために型( RBS )の導入やCIの実行速度の改善などの施策に取り組んでいる。 須貝( @sugaishun ) バックエンドエンジニア 2022年1月入社。スポットワークシステム領域でエンジニアとして活動。 Ruby on Rails ChapterのChapter Leadも務める。Leadとついているが偉いわけではない。好きな言葉は「容赦ない リファクタリング 」 では、ここからが本編です。 タイミーの Rails アプリは60点 須貝:以前Slackでタイミーの Rails アプリケーションが健康かどうかという話をしたと思うんですけど、ここでは健康 = アプリケーションの保守性が高い状態ということにして話をしていきたいと考えています。難波さんはけっこう厳し目の評価をしていたと思うんですけど、主観で構わないので今のタイミーの Rails アプリケーションを100点満点で点数をつけるなら何点くらいなんですか? 難波: 60点 くらいですかね。 須貝:やっぱりけっこう厳しい。 神速: 高専 だと 赤点ギリギリ ですね。 難波:不可ではない、という感じです。入った当初の印象は60点くらいでこの1年で色々取り組んできて65点になったかな、というイメージです。タイミーはプロダクトとしては成長できているし、頻繁に障害が起きているわけでもない。世の中に価値を提供できているのでその点では合格だとは思います。 ただ、ここから上を目指していくためには、まだ我々は合格ラインのギリギリ上くらいなんだぞ、と。そういう想いも込めてこの点数にしています。 須貝:具体的にどの辺りが伸びしろだと考えているんですか? 難波:ひとつは Sentryのアラート ですね。僕が考えるヘルシーなプロダクト組織だったら基本的にはSentryのアラートは鳴らないし、鳴ったらみんながそれを直すべき対象だと思ってできるだけ直す。で、直したらちゃんとresolveして、というのができるようになったらもっと点数が上がるかなと思います。 もうひとつは スロークエリが管理できていない 。スロークエリが出ているということはユーザー体験に直結することなので、 バッチ処理 などであればまた話は別ですけど、現在はきちんと管理できていないのは課題ですね。 最後は 自動テストの信頼性 です。テストが全部パスしているということは本番にデプロイしても大丈夫ですよねという状態を期待しているんですが、テストがパスしているから大丈夫とはあまり思えない。 須貝:たしかにテストの信頼性は僕も課題に感じています。テスト カバレッジ 自体は低くはない *1 んですけど、仕様の カバレッジ が高いかというとちょっと不安なんですよね。 反対に良い点は何でしょうか? 難波:システムが モノリス でやっていけているところは良いところだと思います。マイクロサービスと比べると モノリス *2 のほうが保守性が高いと思っているので。 あとはアプリケーションの保守性に直結するわけではないんですが、CIが速いことでしょうか。CIが速いイコール何かあったらすぐわかるので色々なトライもしやすい。結果的に保守性に寄与する良いところだと思います。 須貝:CIは今何分くらいでしたっけ? 神速:デプロイまで含めると12〜13分くらいですかね。CIだけで見ると8、9分くらいだと思います。 難波:最後にもうひとつ、 github -flowで運用できていることも良い点だと思います。いわゆるgit-flowのようにreleaseブランチ的なものを用意して、QAしてmainブランチにマージしてデプロイというのもそれはそれで良いやり方だとは思います。ただ、mainブランチにマージしたら即デプロイという github -flowはコードの品質をみんなが信頼していないとできないことだと思います。 カナリア リリースが普通、みたいなチームを目指したい 須貝:神速さんは100点満点で点数をつけるなら何点くらいですか? 神速:45とか50点ですかね。 須貝:だいぶ低いですね(笑) 神速:自分の中での100点となると GitHub 社みたいにバンバン本番にデプロイして、 カナリア リリースも当たり前で、みたいな組織です。さらにテストコードも Rails 本体の リポジトリ と同じレベルのクオリティで書く。それが私の中の理想で、それと比べたら今は45点くらいかなと。 須貝:デプロイの話が出てくるあたりが面白いですね。神速さんらしいといいますか。 神速:これは私がインフラもやっているからとかではなく、 顧客へ価値をいかに早く届けるか ですとか、障害が起きた時もいかに早く直すかというのを大事にしていて、デプロイが大事だと思っているのが根底にあります。 須貝:神速さんはどういう状態が健康と考えていますか? 神速:健康さでいうとまず、 Ruby と Rails の最新のバージョンを使うことです。あとはこれに付随して、 Ruby と Rails を最新のバージョンにするためにgemのバージョンを上げ続ける運用があるか。dependabotの整備などもこれにあたります。 あとはテストコードがあるとかレビューをしている、とかでしょうか。難波さんに比べてだいぶ基準が低いんですけど(笑) 須貝:これは過去に経験してきた現場によって基準は変わってきそうですよね。テストを書いているとかレビューしているとか、自分は当たり前だろうと思うんですけど勉強会などで他社のエンジニアと話すと「テストないです」「レビューもないです」というのは普通に耳にします。 神速:ただそこを基準にするよりは もっと上を見たい ですね。 カナリア リリースが普通、みたいなチームを目指したい。 須貝:タイミーの良いところは? 神速:難波さんが挙げてくださった以外だと、 CIでRailsのedgeを使ってテストを走らせている のはレアなのでそこは良いところです。それに付随して弊社のエンジニアが Rails の最新機能を教えてくれる、みたいな福利厚生がある。 後は RBS のような最新機能を導入することにみんなポジティブで、止める人がいない。 やってみようという空気がある のは良い点です。 須貝:やっていきのある人が多いですよね。だから一回やってみましょうという文化がある。逆に伸びしろは? 神速:さっきデプロイ時間の話がありましたけど、私はもっと速くしたい。今はデプロイまでの時間が長いと思っています。 Rails アプリの話でいうと、難波さんと同じでテストの書き方とか保守性はもうちょっと考えたいですね。バグがあったらバグを再現するテストを書くとか。機能を追加する時も既存のテストに手を入れるのではなく、テストを追加して カバレッジ を高めるような考え方は広まってほしいとは思います。 須貝:パフォーマンスの自動テストはちょっと難しそうですね。 神速:でもたとえばコミットログに残すとかはできそうですけどね。パフォーマンスのテストは増やさないにしても、 Rails 本体でもパフォーマンスを計測してコミットメッセージに残したり、 SQL のEXPLAINの内容を貼ったりとできることはあります。「私が速くなったと思うから」ではなくて根拠はほしい。 他の伸びしろを挙げるならレビューの仕方ですかね。「良さそう」でapproveではなく、パフォーマンスや保守性まで考慮したレビューがほしい。それを気にする人が増えてほしいです。ただ良いレビューをする方法を私が教育したいかというとそれは難しいな、という気持ちがあります。 難波:その気持ちはよくわかります(笑)。 須貝:課題感はあるけど自分が率先してやりたいわけではないという。そういったものは Rails に限らず社内にはいくらでもありますよね。 ここまででタイミーでの開発の良いところや伸びしろについて語ってきましたが将来的にこうなっていてほしいというイメージを伺えますか? 難波:なかなかすぐに実現するのは難しいと思うんですけど、 良い設計のシステムにしていきたい ですね。例えば RSpec のcontextがわかりやすいんですけど、contextが多重に 入れ子 になっている時はテストコードの問題というよりはテスト対象クラスの複雑性の問題なんですよね。責務が適切に分解されていないのがテストコードに表出してしまうという。そういうことが起きないようなアプリケーション設計になっていってほしいなと思っています。 須貝:頑張ります。だいたいChapter Leadの僕に返ってくる話ですね。 神速:ただジュニアなエンジニアが頑張っても難しいところはあるので、できる人が教えてあげる環境は何かしら作らないといけないですよね。 難波:そうしたほうが良いというのはすごくわかります。それでいうと今タイミーでは各Squad(チーム)が独立して開発できることを良しとしていて、そこと先輩が教えて知識を伝搬していくことを両方満たそうとすると各Squadに最低一人はミドル〜シニアレベルのエンジニアがいる必要が出てくる。それはけっこう難しいですよね。 須貝:なのでシニアなエンジニアの方、ぜひ弊社に来てくださいという。ちょっとオチがついたっぽい感じになったので今回はこの辺にしておきましょうか。 おわりに いかがでしたでしょうか。弊社のエンジニアは基準が高いなと思いましたね。やっていくしかない。タイミーでの開発についてもっと知りたいという方、ぜひカジュアル面談でお話しましょう。 product-recruit.timee.co.jp 最後になりますが、執筆にご協力くださった難波さん、神速さん本当にありがとうございました。 *1 : 2023/11/27現在、ライン カバレッジ で91.5% *2 : タイミーでは現在モジュラ モノリス 化を進めている
こんにちは! shun です。私はタイミーのデータ統括部でデータ分析やLooker開発を担当しています。今回は、社内のLooker利用者へのユーザーインタビューを実施し、得られた知見についてお話しします。 背景と目的 データ統括部では、各組織がデータを元に意思決定の質やスピードを向上させビジネス インパク トを生むことを目指して、BIツールとしてLooker上のデータ探索環境を開発、提供しています。 Looker開発依頼の相談窓口の設置や利用者向けの講習会の実施、利用者数のモニタリング等をしていますが、Lookerの使い方や課題感やデータを使った意思決定へのハードルについて、実際の声を聞く機会がなかったため今回のユーザーインタビューを実施しました。 取り組みの全体の流れ 具体事例 ユーザーインタビューを通じて最も顕著な課題としてあがったのが、探索環境内のディメンジョンやメジャーがわかりづらく、意図したデータをスムーズに抽出することが難しいというものでした。 あらゆるディメンジョンやメジャーを開発してニーズへの網羅性を高めることの トレードオフ として、全体のわかりやすさを阻害している現状がわかりました。データ統括部のメンバーはデータへの知見が深く、わかりづらさに気付きづらいという側面もありました。 こちらの課題に対しては開発チームとして、文言を社内 ユビキタス に揃え検索性を上げる、よく使うであろうディメンジョンをまとめる、 ユースケース に沿ったテンプレの ダッシュ ボードを作成するなどの解決を進めている段階です。 工夫したこと インタビュー対象者の選定 Lookerの利用ログを活用して、平均的な利用頻度の利用者を見繕ってアポを取りました。今回の目的としては ボリュームゾーン の方の動向を知りたかったためです。バリバリ使いこなしていたり、ほとんど使っていない利用者だと想定していたインタビューができなかったはずなので、 定量 的な観点から意図した対象者を選定することは大切だと感じました。 実際にデータ抽出をしてもらう 画面共有してもらいながら実際に作業する様子を見せてもらうことで、利用者が詰まる ボトルネック が明確になりました。まさに百聞は一見に如かずな体験でした。 個々のインタビュー結果を持ち寄り、共有知をつくる インタビューをやって終わりではなく複数回実施した後、結果を持ち寄り課題の共通認識を作り、解決のア イデア をブレストする会を実施しました。そして、この場で出たア イデア を開発タスクとして起案し改善するというサイクルを回せました。 今後に向けて タイミーではPMMを中心にユーザー ヒアリ ングを行う風土があり、仕組みやナレッジが蓄積されていたおかげで、横展開を比較的スムーズにできLookerチームでもユーザーインタビューが実施できました。もっと詳しく知りたい方は下記ブログもご覧ください。今後も定期的なユーザーインタビューを実施し、利用者に寄り添ったLookerの環境の開発を進めていければと思っています。 tech.timee.co.jp We’re Hiring もっと具体的に話を聞いてみたい方、データアナリストやLooker開発について、興味を持って頂けた方がいらっしゃればお気軽にお話ししましょう!カジュアル面談からご連絡いただけるとうれしいです product-recruit.timee.co.jp
はじめに こんにちは! タイミーのデータアナリストのYoです。 今回は、社内で実施した「Looker講習会」を紹介させていただきます。 ご紹介する内容は以下になります。 Looker講習会について なぜLookerか Lookerとは、Google社が提供するBusiness Intelligenceツールです。 弊社では以下の観点から、社内のデータ利活用ツールとしてLookerを採用しています。 SQLを書くことなく、データが確認できる。 閲覧する人によって、利用するデータの定義に相違が生まれない。 一つのBIツールで様々な観点から、データの活用ができる。 なぜ講習会を実施したか 社内のデータ利活用ツールとしてLookerを採用した一方、Lookerを実際に利用できる人が少なく、またどのようなことができるかもわからないという状態になっていました。 そのため、主に以下を目的とし、講習会を実施しました。 新規Looker利用者の増加 既存Looker利用者のスキル底上げ 講習会の内容について 先述の通り、タイミーでは、Looker自体を実際に利用できる人が少なく、どのようなことができるかもわからないというメンバーが多い状態でした。 そのため、Lookerの編集権限を持つ営業組織のメンバーを対象として、以下のような内容で講習会を実施しました。 Lookerを利用する前に、テーブルや、Lookerで利用できるデータの構造について共有しました。 また、上記をベースとしてLookerの利用方法の説明と、ハンズオン形式での演習を実施しました。 実施後の反響について 50名弱の方に講習会へ参加をしていただき、アンケートを通して以下のような声をいただきました。 残課題 社内のデータ利活用に向けて、一定の効果があった一方で、残っている課題もあります。 1回限りの講習会の実施だったため、今後新しく入社した方へLookerが浸透しない。 基礎的な講習会だったため、応用的なLookerの使い方を網羅できていない。 上記の課題の解決に向けて、継続的な講習会の実施や新入社員のオンボーディングへの組み込み、レベル別の講習会実施などを検討していきたいと思います。 まとめ Looker講習会の紹介はいかがでしたでしょうか。 一般的に社内でのデータ利活用が進むと、データを元にした意思決定が広まっていく一方で、意思決定の基礎となるデータのガバナンスが取れないなどの問題が発生します。 その中で、データアナリスト側がコントロールできるBIツールの講習会を実施することで、データアナリストと社内の他のメンバーの両者にメリットがあります。 データアナリストとしては、単純なデータ抽出依頼ではなく、より高度な分析に注力できます。 また、社内メンバーとしては、データアナリストに依頼をすることなく、自身の意思決定に正確なデータを利用することが可能となります。 タイミーは事業、組織ともに拡大傾向にありまして、データアナリストが活躍する機会が多く存在しています。 これからもデータアナリスト発信で社内のデータ利活用に貢献できる仕組みを展開することで、事業の成長に貢献したいと考えています。 We’re Hiring! 私たち、データアナリストと一緒に働く仲間を募集しています。 まだ転職を考えていない方も、ぜひ一度、お話ししましょう! product-recruit.timee.co.jp 最後に、少しでもこの記事を「いいな」と思っていただけた方は、 SNSへシェアをお願いします!
タイミーの新谷、江田、酒井、正徳です。 Kaigi on Rails 2023 が10月27日、28日の2日間で開催されました。タイミーは去年に引き続きKaigi on Railsのスポンサーをさせていただきました。 また、タイミーには世界中で開催されている全ての技術カンファレンスに無制限で参加できる「Kaigi Pass」という制度があります。詳しくは以下をご覧ください。 productpr.timee.co.jp この制度を使ってオフラインでは4名のエンジニアが参加しました。 参加して聞いたセッションのうち印象に残ったいくつかをピックアップしてご紹介します。 生きた Rails アプリケーションへの Delegated Types の導入 docs.google.com Rails 6.1から導入されたDelegated Typeに関する発表でした。 デプロイ履歴を管理する機能において、AWSのECSやLambdaなどのサービスごとに固有で設定したいデータと、各デプロイ共通のデータ管理をする上で単一テーブル継承(Single Table Inheritance: STI)ではなくDelegated Typeが活用されていました。 タイミーでは一部のテーブル設計でSTIが活用されています。ただ、すべてのサブクラスのすべての属性を持つ単一のテーブルが作られ、nullが避けられない等のデメリットはよくある話だと思いまして、発表の中でも触れられていました。 一方Delegated Typeではスーパークラスの共通データ、サブクラスの固有データを格納するテーブルがそれぞれ作成されます。ポリモーフィック関連をベースにした委譲による実装がなされ、個別データを持つ側のクラスからアソシエーション(今回の発表では has_one)で辿れる仕組みでRails開発者としても直感的に使える仕組みだと感じました。 また各種Gem側での対応も順調に進められており、特にFactoryBotで詰まることなくデータ生成が出来るのは開発・運用の観点でも心理的ハードルが下がる話でした。 そもそもRails 6.1からActiveRecordに導入された機能で、まだ実際に稼働中のプロダクトでの実例もあまり聞いたことが無かったので具体例と共にその特徴を知ることができ、有益な時間になりました。実際に導入する際にはまたこの発表を参考にさせていただこうと思います。 (@edy2xx) 生きた Rails アプリケーションへの delegated types の導入 by mozamimy - Kaigi on Rails 2023 やさしいActiveRecordのDB接続のしくみ speakerdeck.com MySQLへの接続確立するまでのActiveRecord内部の動きを、findメソッドをデバッグ実行した時のスタックトレースを参考に、接続に関する主要なクラスの役割を紹介しながら、内部処理を学ぶセッションでした。 ActiveRecordを用いるとdatabase.ymlに必要な情報を載せるだけでDBにアクセスしデータ取得や操作を行うことができるためあまり意識をすることがありませんでした。しかし、このセッションを通して改めてどのようにDBへの接続を確立・管理するかや、クラスごとの責務を理解できた気がしました。 後半の知見でも触れられていた事ですが、この内部処理を理解することで、接続プールはDBの接続情報を保持したインスタンスを持つため(端折っています)、フェイルオーバーなどでDBホストの変更を行っても、変更以前の接続情報を用いてしまう可能性があります。そのような可能性があることを知っておく事は運用において大事ですし、そのような事態になった時や、そうならないための打ち手を常に持っておく事も重要です。 最後に軽く触れられていたバリデーションクエリの他にも、Railsの再起動等を通して再接続を試みたり、一時的にコネクションプールをやめて都度接続にしたり、クエリ実行時にエラーが発生した場合にエラー内容によってリトライをかけるなど色々あるので改めて理解を深めようと思いました。 (@pokohide) やさしいActiveRecordのDB接続のしくみ by kubo - Kaigi on Rails 2023 Simplicity on Rails - RDB, REST and Ruby speakerdeck.com 偶発的な複雑性が少ない状態をシンプルと定義した上でテーブル設計・API設計・クラス設計の観点でどんなことを意識すべきかを紹介するセッションでした。 テーブル設計:リソースエンティティだけでなくイベントエンティティを見出してhas_many through 関連を作ること。 API設計:テーブルとリソースは似ていることもあるが同じではないので、テーブルに対応しない Controller を作ることは問題ないので気にしなくていいこと。 クラス設計:Controller の create アクションは INSERT INTO 以上のことをしても良いこと。Controller が複雑になってきたと感じたら FormObject の利用を検討すること。 事業としてはイベントエンティティがコンバージョンポイント(お金に変換できるデータという理解をしました)であるという話は確かになと思いました。タイミーのコンバージョンポイントは働くワーカーさんが求人に申し込むマッチングなのですが、ここに対応するモデルは UserOffering という User モデルと Offering モデルの多対多を管理するリソースエンティティとしての命名になっています。様々なものに紐づいているモデルなので変更を加えるのは大変ですが、長期的には手を加えていきたいと思いました。 イベントエンティティをモデルとして表現するという話は texta.fm でも耳にしていて、今回の発表を聞いて更に理解を深めたいと思ったので texta.fm での話の元になっていたパーフェクト Ruby on Rails を読んでみようと思います。 (@euglena1215) Simplicity on Rails - RDB, REST and Ruby by MOROHASHI Kyosuke - Kaigi on Rails 2023 Railsの型ファイル自動生成における課題と解決 speakerdeck.com Railsアプリに型を導入するときに起きるいくつかの課題と、それらの課題に対する解決策を紹介するセッションでした。 課題: Railsアプリに手軽に型を導入したい -> 解決: orthoses-rails 課題: アプリケーションコードの型が無い -> 解決: rb prototype 課題: YARDを活用したい -> 解決: orthoses-yard 課題: YARDのシンタックスをチェックしたい -> 解決: rubocop-yard 弊社でも型(RBS)を活用しているため、課題に共感しながら聞いていました。 Orthoses は使用していませんが、いくつかの解決策は弊社の開発環境でも活用できそうなので、Orthoses への乗り換えも含めて検討・参考にしたいと思いました。 また、発表の後に3Fの休憩部屋(ROOM 0)で今後の型に関する話をすることができて、個人的に型に対するモチベーションが上がる1日になりました。 今後は型(RBS)に関するOSSにも少しずつコントリビュートしていきたいと思います。 (@sinsoku) Railsの型ファイル自動生成における課題と解決 by Yuki Kurihara - Kaigi on Rails 2023 管理機能アーキテクチャパターンの考察と実践 speakerdeck.com 後追いで作ることになりがちな管理機能を Frontend, Backend のソースコードを置き場を元にいくつかのアーキテクチャパターンに分類しそれぞれのpros/consをまとめたのちに、B/43ではどのアーキテクチャを選択したのかと選択した理由・振り返りを共有するセッションでした。 タイミーは ActiveAdmin を用いてモノリシックコードベースに管理機能を密結合させています。ActiveAdmin を使う上で色々と大変な面はあるのですが、なんだかんだ ActiveAdmin は便利ということで使い続けています。とはいえ、過去・現在に最適なアーキテクチャが未来も最適である保証はないので、今後の管理機能の在り方を考える上で非常に参考になりました。 また、管理機能のバックエンドとエンドユーザー向け機能のバックエンドを同じにするのも同意です。管理機能の中には本来は機能として提供すべきなものが提供されていないことから運用に負がたまって社内管理機能に機能追加の力学が働き、結果として管理機能が肥大化していくというのはあるあるだと思っています。バックエンドを分けると管理機能に閉じた変更の方が容易になり、管理機能の肥大化を加速させる要因になりうると感じました。 (@euglena1215) 管理機能アーキテクチャパターンの考察と実践 by ohbarye - Kaigi on Rails 2023 オフライン参加楽しかったですね。2日間で多くの企業のエンジニアとも交流が出来ました。 今年は抽選に外れたのでブース出展できなかったのですが、来年はブース出展したいと思います。来年はブースでまた会いましょう!
はじめに こんにちはokodoonです タイミーのデータ基盤に対してデータモデリングを始めてしばらく経ったので、現状の全体構成を紹介したいと思います 全体構成 弊社のBigQueryは以下の4層にレイヤリングされています それぞれの役割は以下のような切り分けになっています レイヤー名 役割 データレイク層 複数ソースシステムのデータを未加工の状態でBigQueryにロードする宛先 dbt snapshotによるソースの履歴化 ステージング層 複数ソースシステムのデータを共通した処理でクレンジングする層 DWH層 ソースシステムのデータ形式を分析に適した形に変換する層 ディメンショナルモデリング/ログテーブルをイベント単位に分割/その他便利テーブル作成 データマート層 特定用途に対して1:1で作成されたテーブル群を格納する層 ダッシュボード用テーブル/Looker用テーブル/GoogleSheets用テーブル/外部サービス連携用テーブル 図で表すとこんな感じです DWH層, データマート層の切り分けについて 「DWHとデータマートとは?どう切り分けるか?」と100人に聞いたら100通りの答えが返ってきそうなこちらの定義 弊社内ではデータ基盤チーム内で議論した結果、以下のような切り分けをしています - DWH: ステージングデータを分析ニーズに合わせて加工したもの - データマート: 特定ニーズに対して1:1で作成されたテーブル なので ディメンショナルモデリングなどの分析用データへ変換したテーブル クエリを発行する際に、結果の出力が重くなってしまうテーブルを軽量化したもの などがDWH層に格納されているテーブル群で 特定ダッシュボード用に作られたテーブル Looker探索環境用に作られたwide_table 外部サービス連携用に成形されたテーブル などが現在データマート層に格納されているテーブル群になります ※「ダッシュボードに1:1で対応するマートを作る方針なのかあ」と思われるかもですが、こちらは現在推進中のdbt exposureを用いたアウトプット管理を見据えているためです(またこの辺りは別の機会にアウトプットします!) 各層の説明を少しだけ深掘り データレイク層 この層でdbt snapshotを活用した履歴化を実行しています 下流のステージング層/DWH層などで履歴化をしていない理由としては、何かしらの加工がされたあとのデータを履歴化する場合、加工処理に何かしらのバグや想定漏れがあった場合のロールバックが大変なためです 分析に使用する場合は以降の層でクレンジング/モデリングされたデータのみでいいため、基本的に分析ユーザーはデータレイク層へのアクセス権限を保持していません ステージング層 以下のような処理を加えて、生データをクレンジングしています データ型の変更と統一 パーティショニング/クラスタリング データマスキング 品質担保のテスト uniqueテスト Nullテスト 外部キーテスト ユーザーが3NF形式のデータに対してクエリを書く場合は、品質が保証されたステージング層のテーブルに対してクエリを実行してもらうような運用にしています DWH層 DWH層内は以下のような区分が作られています DWH層内区分 役割 dbtのフォルダパス events ログテーブルを分析要件にあるイベントごとに分割整理して軽くしたテーブル群 models/dwh/events component ディメンショナルモデリングのデザインパターンに当てはまらないが便利な部品のようなテーブル群 models/dwh/component pre_dimension 同一属性単位にドメイン情報を結合 ドメイン要件に従ったフラグ生成などの複雑な事前処理 models/dwh/dimension_modeling/pre_dimension dimension SCD Type2カラムの生成( https://en.wikipedia.org/wiki/Slowly_changing_dimension#Type_2:_add_new_row ) サロゲートキーの生成 models/dwh/dimension_modeling/dimension fact ビジネス指標の作成 関連Dimensionのサロゲートキーの反映 models/dwh/dimension_modeling/fact conformed_fact 複数プロセスを跨いだよく使われるfactの組み合わせをconformed dimensionで結合 複数プロセスを跨いだビジネス指標の作成 models/dwh/dimension_modeling/conformed_fact 弊社Dimensional Modelingについて 弊社Dimensional Modelingについてざっくり解説します SCD Type 6の採用 dimensionテーブルは「履歴化されたテーブル(Type2)」と「最新情報を持つテーブル(Type1)」に分けることなく、SCD Type1~3の全てを一つのテーブルで管理する方針としています こうすることでFactに登録するサロゲートキーが少なくなり、分析時のクエリと思考がスリムになると考えています 採用しているFactテーブルのパターン Transaction Fact + Factless Factを接合したFactテーブル Transaction FactテーブルとFactless Factテーブルを別々に作るのではなく、ビジネスプロセス単位でまとめてくっつけています 例えば「買い物プロセス」を仮定した場合に、Transaction Factにあたる 合計売上 とFactless Factにあたる 決済回数 が同一の買い物Factテーブルにあるイメージです Snapshot Factテーブル Transaction FactやFactless Factではうまく表現できないような指標(etc. ユーザー数全体の推移, 銀行残高の推移, )を日,月単位の粒度でスナップショットして保持します 例えば日毎のユーザーdimensionのサロゲートキーを保持するようなSnapshot Factテーブルを作成することで、日単位のユーザー数の推移をDimensionテーブルで絞り込み可能な状態で追えるようになります FatなFactテーブル Factテーブルは分析ニーズのあるビジネスプロセス単位で作られると思いますが、そのビジネスプロセスで発生する指標を全て一つのFactテーブルで完結して出力できるような方針をとっています 買い物の決済プロセスのFactテーブルを仮定すると、 決済Factの決済金額 * 決済Dimensionの消費税率 = 消費税金額 みたいにFactとDimensionを組み合わせて新しい指標を出力するのではなく、決済Factテーブルに 決済金額 , 消費税金額 を二つとも持っておくような形です こうすることで「Factテーブルに定義されている指標をDimensionテーブルで絞り込むだけでいい」「Factテーブルにない指標があればFactテーブルに足せばいい」とクエリ作成時の思考が軽くなると考えています Factテーブル上に社内の指標を全て整理して保持している状態です データマート層 データマート層内区分 役割 dbtのフォルダパス wide_table ディメンショナルモデリング済みテーブルをJOIN済みの状態にしたテーブル群 Lookerの探索環境に1:1で対応している models/marts/wide_table dashboard LookerStudioダッシュボード, Spreadsheetのコネクテッドシートに1:1で対応しているテーブル群 models/marts/dashboard その他 他サービスに連携していたり、特定プロジェクトのために一時的に作られるテーブル群 models/marts/{{その他}} Lookerとの接続について LookerのUIを見てみると、dimensionとmeasureが選択できるような、ディメンショナルモデリングを意識したものになっています そこで弊社ではこの画像のようにLookerのデザインに乗っかる形で、Lookerの接続先をディメンショナルモデリング済みのwide_tableとしており、スター内のFactテーブルの指標をmeasureとして、Factに接続するDimensionテーブル内の要素をDimensionテーブルごとに分割して表示しています こうすることで 商品の生産国が中国の決済金額を決済方法ごとに出力したい というユーザーの思考があった時に 商品の生産国だから => 商品Dimensionの生産国を選択 決済方法だから => 決済Dimensionの決済方法を選択 出力したい指標は決済金額だから => Factの決済金額を選択 とLooker上のユーザーの行動に繋がると思っており、 ユーザーの思考 , 実際に走るクエリ , Lookerの探索環境のUI ができるだけ一致するようなデザインになっています こうすることでLookerへのフィードバックがモデリングをより便利にすることに繋がり モデリングへのフィードバックがLookerをより便利にすることに繋がります ダッシュボード用 / スプレッドシート用のデータマートについて 基本的にマートはView形式で作成します ( 補足:Viewテーブルとは ) Viewとして作成することで無駄なビルドやコスト増加を避ける方針です マート化する判断基準は「正確性と安全性を保守すべき対象かどうか」であり、チーム内で定めた基準に従ってマート化を実施してアウトプットをコード管理します ダッシュボード用 / スプレッドシート用のデータマートをdbtで作成するのでdbt上でリネージュ管理が行えるようになり、破壊的な変更が上流で発生した場合に保守対象のダッシュボードへの影響を抑えることができます 作成したマートに対応するdbt exposureを宣言することでデータオーナー管理やアウトプット先管理も行えるようにする予定です 将来構想 数ヶ月以内に実現したい将来 dbt exposureによるオーナーを含めたアウトプットの管理 主要ビジネスプロセスの全モデル化 1年以内とかにはやりたい将来 サンドボックス層の作成 ポリシータグを用いた、メンバーのアクセス可能情報レベルに応じたクエリ範囲の制限 まとめ・今後の課題 弊社は組織拡大のスピードがとても早く、データ活用者数もとても多いので クエリ作成者間で出力される指標値がブレる。ダッシュボードやLookerの値とアドホッククエリの値がブレる。そもそも定義がブレている などの問題を防ぎつつ 簡便にあらゆる指標値がクエリできる ように、ディメンショナルモデリングを主軸としたデータモデリングを推進してきました 現在は作成したモデル数はかなりのものになり、それに従ってLooker環境もリッチになってきたのですが、アナリストが作るアドホッククエリやダッシュボード作成クエリにまだまだ採用してもらえていない課題感があります モデリング済みテーブルを使ってクエリを書くのはDWH開発に参加してくれているアナリストメンバーに集中していて、他アナリストメンバーにモデリングなどの技術的内容とDWHの開発内容などをうまく同期できていない状態です モデリング済みテーブルを使用することで 指標が全社定義と常に一致する Dimensionの履歴を考慮した指標出力が可能になる クエリ文量が1/5とかになる クエリパフォーマンスが向上するケースもある などメリットも大きいと思うので、モデリング手法の理解を助けるアウトプットやワークショップなど様々なHowを通して、アドホックなクエリに採用してもらってもっとフィードバックのサイクルが高速化するようにしていきたいです! We’re Hiring タイミーのデータ統括部では、ともに働くメンバーを募集しています!! product-recruit.timee.co.jp
イベント概要 2023年8月2日に「What’s “Next” JS Meetup」と題してバベル社のuhyoさんをお招きしてNext.jsに関する勉強会を開催しました。 その中でタイミーフロントエンドエンジニアのいーふとさん(@redshoga)の講演をイベントレポートにまとめてお届けします。 今回のスピーカー Server Actionsの概要 ※ 2023年8月2日のイベント開催時点での情報であり、将来的に変わる、変わっている可能性があります。 Server Actionsはざっくり説明するとサーバーで動作するコードをあたかもクライアント上に記述できる機能です。コード下部のLikeボタンがクリックされると、propsに渡されているincreateが呼び出され、その中でPrismaのclientが動作します。 クライアントで動作 レンダリング結果 サーバーで動作 送信データ 一つのファイルだけでなく別ファイルで書くことも可能で左側がクライアントで動作、右側がサーバーで動作します。それぞれに”use client” “use server”というディレクティブを書く必要があります。 レンダリング結果を見ると普通のフォームとして動作することがわかります。右側の”createPost” というのが普通のTypeScriptをインポートしているにも関わらず通常のformとして動いてくれる不思議な機能です。 送信データとしては普通のフォームデータです。内部ではACTION_IDがよしなに付与され、良い感じに動作します。 既存のフルスタックTSのアプローチ ここまでがServer Actionsを踏まえた未来のTypeScriptをイメージした内容だったのですが、現状のフルスタックTypeScriptにはどんなアプローチがあるのかを見ていきます。 よくあるのがスキーマの活用です。概念としてはOpen APIやGraphQLとも一緒だと思います。スキーマAPIの定義ファイルからTypeScriptの型生成を行う。よく見る代表例としてはopenapi-typescriptなどですね。タイミーではopenapi2aspidaというライブラリを使っています。aspidaというライブラリがあってそれに変換してくれるものですね。当然、定義ファイルが必要になりますが、自動生成される仕組みにしてしまえば楽なのかなと思います。 他の例としてはtRPCが今、流行っているのかなと思っています。TypeScriptに特化したRPCでインターフェースを提供するライブラリという解釈です。Next.js、Express、FastifyにインターフェースとしてつけてあげるとあたかもRPCのような感じで書くことが可能になります。フロント側の対応もしっかりしてReactのhookを勝手に提供してくれる優秀なライブラリだったりします。 シリアライズもsuperjsonが用いられており、MapやSetといったオブジェクトをそのまま送ることが出来ます。 クライアントサイド レンダリング結果 サーバーサイド 送信データ こちらが実際に書いた例です。 クライアントサイド側にapi.example.hello.useMutationというのがありますが、これがサーバーサイド側のコードを書くと勝手に生成されているような感覚で書くことができる開発者体験のよいライブラリになっています。 シリアライザとしてはsuperjsonが用いられており、内部ではよしなにwrapされているので使用感としてRPCっぽいといった感じのライブラリになります。 ここまでServer Actionsについてと今までのフルスタックTypeScriptをどうやって書いていたのか、という話をしてきましたが、比較するとServer Actionsは圧倒的に手軽です。”use server”と書くだけで動くので手軽です。 シリアライズの方式も微妙に違っていて、レスポンスで返ってくるやつを色々実験しているのですが、JSXとかも実は送れたりします。 Next.js特化で共通のインターフェースみたいな概念ではないので今のところはServer Actionsを書いて、それをNativeAppから呼ぶみたいなことは想定されていないと思います。revalidatePathの様な所謂、Next.jsのキャッシュサービスと併用して使ってねみたいなサンプルコードとかも散見されるので、インターフェースを提供するというよりはやはりNext.jsに特化したRPCといった印象です。 Server Actionsの活用例 Server Actionsを知っている人は似たイメージを持っていると思いますが、Vercel盛り盛りのフルスタック構成ですね。 サーバーのデータベースを触る際はServer ActionsとかAPI Routesとかで書いて、クライアントコードは普通にReactで書くと。Vercelは最近様々なサーバーレスストレージサービスを出しているのでそれらを活用して永続化していきます。 あとはKey-Valueストアとかもあるのでそれらでキャッシングをしてあげるといった感じになります。 もう一つはBFF as Server Actionsのようなイメージですね。BFF as Server Actionsという言葉自体は私が作った造語なのですが、先程の構成ではネイティブアプリケーションからAPIを叩けないですよね。 それをAPIだけVercelから外に出して、VercelのServer Actionsから叩いてあげる、といったことも可能ではないかなと思っています。あとは単純にServer ActionsにすることでJavaScriptが動かない環境でもフォームとタグが動くのでそういったプログレッシブエンハンスメントを主とした活用もあるのではないかなと思います。 まとめ 今回はSever Actionsこんな感じですよ、ということをお伝えしました。とはいえまだアルファ版なのでServer ActionsちゃんとGAされてほしいなと期待を持っています。便利なので使っていきたいという気持ちですね。最近のNext.jsはフルスタック寄りに様々な機能を提供してくれているんですけど、Railsのような規約(レール)がある訳ではないのでその議論は活発になっていくのでないかなというのを個人的に思っています。 ご清聴ありがとうございました。 uhyoさんの発表や弊社のもう一人の西浦さんの発表が気になる方は以下の動画を是非ご覧ください。 www.youtube.com 少しでも興味を持っていただいた方は是非こちらからカジュアルにお話しましょう! product-recruit.timee.co.jp
内容 こんにちは! スクラム マスターのShinoP ( @marupopu )です! この記事は、 スクラムフェス仙台2023 で登壇した事によるふりかえり記事になります。 実際の発表内容はこちら speakerdeck.com ※後で動画も公開されると思いますので、ぜひ見てください! なぜ登壇したか? 初めに、なぜ登壇する事となったか?ですが、時は今年の1月に行われた RSGT でコンフォートゾーンから抜け出す体験をしたのがきっかけでした。 具体的なコンフォートゾーンから抜け出す体験というのは、RSGT初参加で、初 OST のホストを務めた事です。 今までは外部の勉強会に参加しても、話を聞くだけで自分の中で解釈をして納得をして、わかった気になって終わっていました。 また、自分は参加する人で、登壇している人たちはすごい人というような印象を持っていました。 そんな中、RSGTならではの空気感や、 OST のホストをやる人も多く存在し、遠方から参加していることもあり、参加するからには効果を最大限にしたいという思いがあり、勢いのまま OST のホストに申し込んだ結果、多くの方が集まって頂き5チーム程度に分裂して話し合う規模になりました。 その時に、自分が思っているような疑問でも共感や気になってくれる人は案外多いのかもしれない…と認識できました。 その後、 タイミー主催の勉強会 での登壇経験をして、共同で発表してくださった Coincheck の方々や、参加者の方とScrumについて語り合う体験が楽しいと感じました。 www.youtube.com 自社主催の勉強会での登壇が楽しいと感じたので、さらに課外活動などにも興味が湧きました。 なぜ、 スクラム フェス仙台か? さて、なぜ スクラム フェス仙台を選択したのか?というと、私自身が 岩手県 盛岡在住ということもあり、仙台も割と近いので現地参加しやすかったのと、4年前に東京から岩手に移住してくるほど東北が好きで、東北という括りで盛り上げたかったのが理由になります! あと、去年も参加したかったのですが、現地チケット取れずに断念した記憶があるのでリベンジも込めて。 雰囲気はこんな感じ スクフェス 仙台間もなく始まるサメー🦈 pic.twitter.com/nVx7xLKmtK — スクラム フェス仙台 (@scrumsendai) 2023年8月25日 初プロポーザルと文豪制度 もちろん、初プロポーザルでした! Scrum Fest Sendai 2023 - SMって兼任?専任?結局どうなの? 〜持続可能なチームになる為の鍵〜 | ConfEngine - Conference Platform どのように書いたら良いかわからなかったので、他の方の書き方などを参考に、自分の伝えたいことを書いてみました。 また、採択頂けたタイミングで資料作成などを行うにあたり、弊社のTDE10の中の人つの”文豪制度”を利用しました。 productpr.timee.co.jp 壁打ちをしてもらいながら資料を作成し、本当に自分が伝えたいことを 言語化 するのを手伝って頂き、本当に助かりました。 また、壁打ちを行なっていく過程で、最初に出したプロポーザルのアウトラインがずれていたりもしたのに気が付いたので、何を伝えたいのか?から正しく伝えられるような 言語化 、資料作成までお世話になりました。 登壇者での参加は何がよかったか? RSGTなどもそうでしたが、登壇者でない時の参加と登壇者としての参加の違いを感じたので書いていこうと思います。 まず、気持ちの問題かもしれないのですが、”圧倒的喋りかけやすさ”があるような気がしました。 何人かに、タイミーのしのぴーさんですよね?って話しかけられて嬉しかったです😆 めちゃめちゃ目立つ登壇者用TシャツもGOOD! また、認定 スクラム マスター研修やクローズドなイベントでの交流のあった方々にも声をかけて頂いたりして、登壇者 = 参加することが確定しているなどのイメージなので、話しかけて頂いたのかな?と推測しています。 あとは、同じ登壇者同士で話せたことも良かったと思います。 初めての登壇者同士で話したりして、緊張しますね…とかその時の心境をシェアできたのはとても良かったです。 その他よかった点 こちらは登壇者でなくとも スクラム フェス仙台2023の体験がよかった点を書いていきます。 まず、なんと言っても” クラフトビール ”が美味しかった🍻🍻 (ビールを飲みながらの OST 最高でした!!!) Day1で少し飲みすぎて、Day2に酒焼けで声が少し枯れてしまったのは反省😂 ノベルティ のシャークレくんグラス 来週8/25,26は スクフェス 仙台! ノベルティ を製サメしてます🦈オンサイトの参加の方はぜひ現地で乾杯🍻しましょ〜 #scrumsendai pic.twitter.com/s7BZjrHJBZ — スクラム フェス仙台 (@scrumsendai) 2023年8月15日 また、その場でしか体験できないワークショップ体験や飲み会、昼ご飯時に話されるディープな悩みの相談会なども現地参加ならではの楽しみ方だなぁと思いました。 あとは、会場が複数の階に分かれていたこともあって、ギャザリング感がめっちゃ味わえました。 1階では、メイントラックと品川トラックとオンライントラックがモニターで映されていたのですが、その空間で話している人もいればモニターを見て観戦している人もいたり自由な感じでとても雰囲気が良かったです。 この場作りという観点で、 スクラム フェス仙台2023の運営メンバーが考え抜いたレイアウトだと思うと、すごいと感じておりました。 さらに、スタッフの方々も楽しんでいる&参加者も全員協力的だったと思うので、やはり場を作り上げていくのは全員でやるのだなぁと再認識できました。 反省点 めちゃめちゃ楽しかったのですが、ふりかえっていて、いくつか反省点が見えてきました。 それは、登壇を意識するあまり、他のセッションが頭に入ってこなかった…という点です。 Day1でお酒をたくさん飲んだのも緊張を紛らわすためかもしれません…(酒焼けの言い訳) そして、お気付きでしょうか…現地でのオリジナル写真がほとんどない事に… 精神的に一杯一杯で写真を撮るのも忘れていました… また、Day1ではしゃぎ過ぎて、登壇が終わった後の無気力感と 疲労 が半端なかったです。 (まさに スクラム フェス仙台2022のシャークレくんのツイートみたいなイメージ🤣) それだけ、この登壇にかけてきたという事にしておきましょう😅 初めての社外カンファレンスの登壇終わって、めっちゃ脱力してる 燃え尽きた… — ShinoP/しのぴー@ スクラム マスター (@marupopu) 2023年8月26日 #scrumsendai スクラム フェス仙台のすべての日程を終了しました。 2日間本当にありがとうございました! pic.twitter.com/RPuVsa3NRo — スクラム フェス仙台 (@scrumsendai) 2022年8月27日 今後について 今回、 スクラム フェス仙台に登壇者として参加して、色々な気付きが得られました。 その中で、 カイゼン したい点としては、登壇経験を積み重ねてさらに楽しみたい!ということです。 今回は初参加、初プロポーザル、外部イベントでの初登壇という事でめちゃめちゃ緊張したのですが、楽しみの方が上回る体験となりました。 しかし、もっと楽しむためには登壇に慣れる事により心に余裕を持ってさらに楽しめるのでは?と感じました! (あと、全力で楽しむために体力もつけたい!) 他には、今回の スクラム フェス2023の運営側の方々が光る場面がいくつも目撃したので、自分でもこのような場作りができるようになりたいと強く思いました。 元々、なぜ スクラム フェス仙台か?にも書いた通り、東北を盛り上げたいという気持ちがあります。現在、北東北(青森・岩手・秋田)に スクラム や アジャイル の勉強会はほとんど開催されていないですし、コミュニティも私の観測範囲では見当たらないように見えます。 (コンパスで見ると「 アジャイル ・ スクラム 」のワードで勉強会が一つもない) ですので、個人的なNextActionとして岩手で アジャイル / スクラム コミュニティを生み出す活動なども今後やっていきたいと考えています!! 最後に、こんな素晴らしい場作りをして頂いた運営の方々を初め、関わってくださった全員に改めて感謝をお伝えして、ふりかえりの記事とさせて頂きます!! ありがとうございました!そして、おつサメでした!! スクフェス 仙台2023!おつサメ〜🦈最高に楽しい2日を皆さんと作れて最高嬉しいシャー  #scrumsendai pic.twitter.com/jtNhSak5sz — スクラム フェス仙台 (@scrumsendai) 2023年8月27日
イベント概要 2023年8月24日にGaudiy社との共催で「 不確実性を乗りこなす強いプロダクトマネジメント組織のデザイン 」と題してプロダクトマネジメント組織に関する勉強会を開催しました。 不確実性の高いマーケットに対して、高い成果を出せるチームにしていきたいPdMやPOの方、組織設計からプロダクトのアウトカムを高めたいと考えているEMの方などに特にお勧めの勉強会でしたのでイベントの中から高石さんによる「『わからない』に立ち向かい続け成果を生み出す、実験志向なチームの育み方」の講演レポートをお送りします。 今回のスピーカー 「『わからない』に立ち向かい続け成果を生み出す、実験志向なチームの育み方」 不確実性の高いゴールに付き物の「わからない」 プロダクト開発ではゴールを設定します。タイミーではスクラムを採用しているチームが多いのでプロダクトゴールと表現されます。 RetentionRateの10%向上といった定量的なゴールもあれば、ユーザーストーリーの形式で特定属性のユーザーが○○を実現できるようになるといったゴールもあると思います。近年はこの様に何かしらのゴールに向かってイテレーティブな開発をすることが一般的になってきました。 ここで付き物なのが「わからない」ということです。ゴールが定まってもそこにどうやれば辿り着けるのかは「わからない」に満ちています。PdMやPOの方で「わからない」のフィードバックを貰ったことがない人はあまりいないのではないでしょうか? でもこの「わからない」ことってネガティブなことなのでしょうか? 人間にとって「わからない」ものは本能的に身構えてしまったり、気持ち悪さを感じることが多いと思います。一方でこの「わからない」気持ちは「わかりたい」という行動への原動力でもあります。「わからない」を放置しておくとモチベーションの低下に繋がってしまいますが、適切に解消が出来ればプロダクトの成果に繋がります。 「わからない」を「わかる」にする力がプロダクトの成果を生む 普段、仕事で強く意識することは少ないですが日常業務の中でも「わからない」ことが沢山あります。 こういった様々な疑問がある中で皆さんは無意識に解消する行動を取っていると思います。インタビューをしてみたり、A/Bテストを実施したり、言語化こそされていなくてもわかる様にするアクションを取っているのではないでしょうか。 その様な行動で「わからない」を一つずつ「わかる」に変えていくことで不確実性の高いゴールに対しても道筋が見えてきます。ゴールの達成方法がわかるようになってくるのでプロダクトの成功にまた一歩近づくことが出来ます。 逆に「わからない」を解決出来ないとBIGBANGリリースに繋がったり、出したは良いけどその後の計測が上手く出来ずに成功したかも失敗したかも判断出来ないといったことが発生します。それだけならまだしも「わからない」を解決出来ず、不確実性を許容出来ないチームになってしまうと簡単なゴールしか設定できなくなってしまうリスクもあります。 基本的にどのプロダクトも新規性が高くまだ世の中に無いものを生み出しているわけですから、文化として「わからない」を許容し、それを解消しつづける組織力をいかに創れるのかが企業の競争力に直結すると思います。 タイミーのケースから見る、チームの実験を支える環境づくり ユーザビリティテストやA/Bテストなどについてはよくまとまった書籍がたくさんありますし、皆さんも勉強されていることと思います。しかし、座学として知識を習得しても、それらを実際にチームとして行動に移すまでには辿り着けないことの方が多いのではないでしょうか? そこでタイミーでは「チームが実験を行ったり情報収集する、心理・物理のハードルを極限まで取り除く」ということを行っています。 チーム専属のアナリスト チーム専属になる前までは分析チームに依頼をして、回答を待つ形式でした。この場合、分析チームが忙しい場合だと答えが分かるまで1〜2週間掛かってきます。このようなことになると既にスプリントの1週間を超えてしまいます。依頼側としてもそんなに時間がかかるなら別のやり方を模索しよう、となり依頼することそのものを諦めてしまいがちです。 しかし、スクラムチームに1人専任でプロダクトアナリストが所属する様になったことで物事の大小を問わず、迅速な解決ができる様になり、定量面の仮説検証速度が向上しました。 ユーザーインタビューのハードルを極限まで落とす「 Interview as a Service 」 タイミーではユーザーと話したいと思ったらとにかくすぐに話せるようになっています。 Googleカレンダーにパブリックな仕組みがあってユーザーインタビューの予定を作ると自動で人がアサインされ、議事録が用意されて、インタビューアーのリクルーティングまで完結する仕組みがプロダクトマーケティング部によって運用されています。 このプロセスでは最短で明日、中央値でも2、3日後には望んだセグメントの人とお話が出来ます。 これによりスプリントの中に収まってくるので事前にリサーチして準備をしよう、ではなくスプリントの中で解くといったケースまで出てきたのも面白かったポイントです。 ここまで環境を整えていくと勝手に実験が増えていきます。実験をしよう、しようと思っているうちは中々実験が進みませんが、阻害する要因を全て取り除いていくと皆も実験したい気持ちはそもそもあるので自然と実験が増えます。 実験に特化したフレームワークの採用 『LeanUX』の中で紹介されているLeanUXCanvasを採用しています。 1〜8までのボックスがあるのですが、特に7が面白く「一番はじめに学習しないといけないこと」を明示しています。今回の言葉でいうと「わからないものの中でもっとも早くわかる様にしないといけないこと」ですね。「わからない」ことの存在を事前にフレームワークで宣言をしてその解消の重要性も説いていることがユニークに感じています。 取り組みのとある共通点 どのケースも全く違う取り組みに見えて、共通点があります。 ① ゴール達成の過程に生まれる「わからない」を「わかる」にする力をつけること ② チームを取り巻く、組織や環境面からアプローチしていること 「わからない」を爆速で楽にわかるようにするといった観点でチームや組織を組成すれば高い不確実性も許容して実験を重ねて解決できる強いチームが生まれます。 また「わからない」を解決するだけであれば手段はさまざまありますが、組織や環境面からのアプローチはレバレッジが効きます。例えばインタビューの話であれば開発チームに限らず、営業やマーケティングチームが使ったりしていて、全社に波及しています。 まとめ まとめると今日は不確実性という言葉は広いので「わからない」と置き換えましたが、この「わからない」ことをネガティブに受け取って欲しくないと思っています。「わからない」ことも当然ですが、すぐ「わかる」ことよりも「わからない」に対して挑戦して考えていく方が生産的でプロダクトの組織としてもより成果をあげれるのかなと思います。 実験や仮説検証でも個人だけの頑張りではなくチームとして取り組めることが多くあります。プロダクトのチームは元より実験をしたいので妨げる要因を取り除いていくことで自然と実験する文化が育まれ、不確実性に強いチームになっていきます。タイトルに繋がっていきますが皆で「わからない」に立ち向かい続け成果を生み出す、実験志向なチームを育てていきましょう。 充実のパネルディスカッションは本編動画から! www.youtube.com 実験文化のある組織をいいなと思った方は是非お話しましょう product-recruit.timee.co.jp
こんにちは! タイミーのデータアナリストの@takahideです。 今回は、メンバーが行っている「勉強会」を紹介させていただきます。 ご紹介する内容は以下になります。 ・勉強会ではどんなことをやっているの? ・勉強会を通じた業務の広がりとは? 勉強会について 勉強会の具体例 背景 勉強会で用いた本 勉強会の進め方 勉強会を通して 効果検証の講習会 まとめ We’re Hiring! 勉強会について まずは、勉強会に関して簡単に紹介させてください。 タイミーでは、有志のメンバーが勉強会を立ち上げる文化が根付いています。 その中でも、データアナリストの勉強会は大きく2種類あります。 一つは、「会計」「プロジェクトマネジメント」などテーマをもとにした「一話完結型」のもの。 もう一つは、課題本を決めて輪読する「読書会型」で、今回はこちらをご紹介します。 勉強会の例 「読書会型」の勉強会の一番の目的は「分析力の向上」です。 勉強会を通じて、分析手法に関する共通言語が浸透することで、分析スキルの底上げだけでなく、 分析プロセスや結果に関するコミュニケーションが円滑になりました。 また、他部署の方と勉強会を行うことで、複数部署間のコミュニケーションもスムーズになりました。 プロダクトチームと行なった勉強会の感想 今回は「効果検証」に関する勉強会を一つの事例として紹介できたらと思います。 勉強会の具体例 背景 効果検証の勉強会は、データアナリストの検証手法の習得を目的に始まりました。 タイミーの組織が大きくなるにつれて、検証が必要な施策の件数も増加傾向にあります。 また、マーケティング施策のように、ABテストでの検証が難しいものも増えており、手法の共通認識を作るニーズが高まっていました。 勉強会で用いた本 勉強会では、『効果検証入門〜正しい比較のための因果推論/計量経済学の基礎』(安井 翔太)を課題図書として用いました。 傾向スコア、差の差分法(DID)など効果検証の手法が網羅的に説明されているのと、コードが豊富なのが良かったです。 勉強会の進め方 勉強会は、週に一度、担当者がgoogle collabを用いて発表を行い、他メンバーから質疑応答を行いながら進めています。 コードを確認しながら進めることで理解が深まりやすくなっています。 勉強会を通して 大きく3つの活動に繋がりました。 一つ目は、当初の目的だった「効果検証手法に関する共通言語の促進」です。 特に、検証手法を用いる際の注意点を共通認識として持つことができました。 例えば、回帰不連続デザイン(RDD)を利用する際には、「分析対象が介入に関する状態を操作できない」ことが求められますが、こうした条件が満たされない場合は、逆に、集積分析(bunching analysis)のような操作を前提にデザインされた手法を試すなどです。 二つ目は、勉強会をきっかけに、データアナリスト内でpythonを推進する活動に繋がったことです。 勉強会はpythonで実行しましたが、勉強会の内容を踏まえてタイミーの実データを用いた検証を行う際に、pythonへの慣れの有無がボトルネックになっていることが分かりました。 そこで、現在は、pythonを通常の分析業務で自然に扱えるための学習パッケージの構築を進めています。 三つ目は、全社的な講習会に繋がったことです。 こちらに関して次に詳細をお伝えできたらと思います。 効果検証の講習会 冒頭でもお伝えしましたが、現在タイミーは組織拡大に伴い、効果検証のニーズが増加しています。 そこで、今回の勉強会の内容と、過去の分析ナレッジを踏まえて、全社的な効果検証の講習会を実施しました。 効果検証の目的から始まり、交絡因子など、効果検証で注意するべき内容をお伝えしました。 また、効果検証を進める際に、データアナリストと事業部がどのように連携すると良いかをお話しました。 まとめ データアナリストの勉強会のご紹介いかがだったでしょうか。 改めてにはなりますが、データアナリストが勉強会を行うメリットに「分析手法の習得」と「共通言語の促進」があると思います。 特に後者は、勉強会のナレッジを他部署の方に展開するなど、コミュニケーションの円滑化に繋がっていると考えます。 タイミーは事業、組織ともに拡大傾向にありまして、データアナリストが活躍する機会が多く存在しています。 これからもデータアナリスト発信で意思決定に役立つ情報を展開することで事業の成長に貢献したいと考えています。 We’re Hiring! タイミーのデータ統括部では、ともに働くメンバーを募集しています!! カジュアル面談 も行っていますので、少しでも興味がありましたら、気軽にご連絡ください。
こんにちは☀️ タイミーでアナリストとアナリティクスエンジニアしてますokodoonです 今回の記事はdbt CloudでPull Requestを作るときに、レビュー負荷が高くなってしまっていた問題を解消できるように、コンパイル済みのSQLをPR上にコメントするような仕組みを作成したことについての紹介です。 もし同じような課題感を抱えている方がいらっしゃれば、参考にしていただければ幸いです 課題感 今回選択した解決策 背景/前提 実装概要 各ステップの説明 PRの情報をもとにprofiles.ymlの動的生成 コンパイル処理の実施 PR上にコメント どんなふうに動くかみてみる 結果 We’re Hiring! 課題感 弊社のデータ基盤ではDWH層DataMart層は「分析用に加工されたデータを扱う層」として定義しています。 各種ドメインに依存した集計や変換のロジックが含まれるため、この層のモデリングに関しては基盤開発側のレビューのみでなくアナリスト観点でのレビューも必須となります。 ですが、分析ドメイン観点でのレビューが必要な場合に、純粋なSQLではないdbtモデルがアナリストレビューの障壁になることが多いです。 またアナリスト以外であっても「このmacroがどのようにコンパイルされるか」を把握するのは少し面倒だったりします 今回選択した解決策 そこでdbtモデルをコンパイルしたSQLファイルをPullRequest上にコメントするような仕組みを考えました。 この実装によって先ほど挙げた課題感が以下図のように解決されることを期待して開発しました 背景/前提 1. 開発環境について 弊社ではdbtを活用したデータ基盤の開発を行っており、 dbtモデル開発をdbt Cloud上の統合環境にて実施するような流れになっています。 2. CIジョブについて CI用のdbt CloudのJobは RUN ON PULLREQUEST で実行されて、以下のようなコマンドが実行されています。 dbt build --select state:modified+ mainブランチとの差分があるdbtモデルがBigQuery上にビルドされている状態です(本来CIで走るdbtコマンドをレイヤーごとに分割していますが、ここでは簡略化しています) 3. CI環境のスキーマ名とcustom_schemas.yml戦略 dbt Cloudで設定できるCI Jobではtarget schema名の命名が dbt_cloud_pr_<job_id>_<pr_id> となっており、連携レポジトリのPR番号に対して動的に作成されます。 (DBT_JOB_SCHEMA_OVERRIDEでこの命名規則の上書きもできますが、PR単位でCIテーブルが作成されて欲しいので、デフォルトの命名規則にしたがっています。) 参考: https://docs.getdbt.com/docs/deploy/continuous-integration#how-ci-works また、弊社では開発環境とCI環境のスキーマ名が {{ターゲットスキーマ名}}_{{カスタムスキーマ名}} になるように custom_schemas.sql で定義してあります。 そのため、CI環境のテーブルは dbt_cloud_pr_<job_id>_<pr_id>_{{カスタムスキーマ名}} の命名規則で作成されたデータセット名以下に作成されている状態です。 実装概要 作成したYAMLはこちら(クリックで展開) name : DBT Compile and Comment on PR on : pull_request : types : [ opened, synchronize, reopened ] jobs : dbt_compile : runs-on : ubuntu-latest permissions : contents : read pull-requests : write id-token : write env : DBT_ENVIRONMENT : dev steps : - name : Check out repository code uses : actions/checkout@v3 with : fetch-depth : 0 ref : ${{ github.event.pull_request.head.ref }} - name : Fetch base ref run : git fetch origin ${{ github.event.pull_request.base.ref }} - name : Set Up Auth uses : "google-github-actions/auth@v1" with : token_format : "access_token" workload_identity_provider : "hogehogehoge" service_account : "hogehogehoge@hogehoge.iam.gserviceaccount.com" - name : Set up Cloud SDK uses : google-github-actions/setup-gcloud@v1 - name : Set up Python uses : actions/setup-python@v3 with : python-version : 3.11 - name : Install dependencies run : | python -m pip install --upgrade pip pip install dbt-core dbt-bigquery - name : Generate profiles.yml run : | chmod +x ci/compile_sql_comment/generate_profile.sh ci/compile_sql_comment/generate_profile.sh ${{ github.event.pull_request.number }} - name : Compile DBT id : compile run : | dbt deps dbt compile --profiles-dir . --target dev --profile timee-dwh compiled_sqls="" files=$(git diff --name-only origin/${{ github.event.pull_request.base.ref }} ${{ github.event.pull_request.head.ref }} | grep '\.sql$' || true ) if [ -n "$files" ] ; then for file in $files; do compiled_file_path=$(find target/compiled -name $(basename $file)) echo "Compiled file path: $compiled_file_path" if [ -n "$compiled_file_path" ] ; then compiled_sql=$(cat "$compiled_file_path" ) compiled_sqls="${compiled_sqls}<details><summary>${file}</summary>\n\n\`\`\`\n${compiled_sql}\n\`\`\`\n\n</details>" fi done fi printf "%b" "$compiled_sqls" > compiled_sqls.txt - name : Comment on PR uses : actions/github-script@v6 with : script : | const fs = require('fs'); const output = fs.readFileSync('compiled_sqls.txt', 'utf8' ); const issue_number = context.payload.pull_request.number; const owner = context.repo.owner; const repo = context.repo.repo; async function processComments() { const comments = await github.rest.issues.listComments( { owner : owner, repo : repo, issue_number : issue_number, } ); const dbtComment = comments.data.find(comment => comment.body.startsWith('DBT Compile Result')); const body = ` DBT Compile Result : \n$ { output } `; if (dbtComment) { await github.rest.issues.updateComment( { owner : owner, repo : repo, comment_id : dbtComment.id, body : body } ); } else { await github.rest.issues.createComment( { owner : owner, repo : repo, issue_number : issue_number, body : body } ); } } processComments(); actionsの流れを説明するとこうなります。 PRブランチにチェックアウトして、PRブランチとmainの差分を確認するためにfetch workload_identity_providerを用いたBigQueryへの認証 必要パッケージのインストールと使用するパッケージの宣言 PRの情報をもとにprofiles.ymlの動的生成 コンパイル処理の実施 dbt compileの実行 mainとの差分があるファイルだけを抽出 差分ファイルのcompile結果を文字列化 PR上にcompile結果がなければdbt compile結果を新規コメント。既にcompile結果がコメントされていたらコメントをupdate 各ステップの説明 説明が必要そうなステップの説明をしていきます PRの情報をもとにprofiles.ymlの動的生成 上で述べた通り、CI環境のテーブルは dbt_cloud_pr_<job_id>_<pr_id>_{{カスタムスキーマ名}} の命名規則で作成されたデータセットに配置されています。 dbt compileの出力結果のデータセット名が dbt_cloud_pr_<job_id>_<pr_id>_{{カスタムスキーマ名}} になるように、デフォルトスキーマ名を動的に宣言するためのprofiles.ymlを作成するシャルスクリプトを作成しております #!/bin/bash set -e cat << EOF > profiles.yml timee-ci-dwh: target: ci outputs: dev: type: bigquery method: oauth project: ci_env_project schema: dbt_cloud_pr_39703_ $1 execution_project: ci_env_project threads: 1 EOF 参考: https://docs.getdbt.com/docs/core/connect-data-platform/bigquery-setup#oauth-via-gcloud これによりdbt compileの出力結果が、このターゲットスキーマ名を参照したスキーマになります。 コンパイル処理の実施 run : | dbt deps dbt compile --profiles-dir . --target ci --profile timee-ci-dwh compiled_sqls="" files=$(git diff --name-only origin/${{ github.event.pull_request.base.ref }} ${{ github.event.pull_request.head.ref }} | grep '\.sql$' || true ) if [ -n "$files" ] ; then for file in $files; do compiled_file_path=$(find target/compiled -name $(basename $file)) echo "Compiled file path: $compiled_file_path" if [ -n "$compiled_file_path" ] ; then compiled_sql=$(cat "$compiled_file_path" ) compiled_sqls="${compiled_sqls}<details><summary>${file}</summary>\n\n\`\`\`\n${compiled_sql}\n\`\`\`\n\n</details>" fi done fi printf "%b" "$compiled_sqls" > compiled_sqls.txt dbt compileをPRブランチで実行 git diffで差分があった.sqlファイル名をdbt compileの実行結果であるcompile済みsqlファイルが格納される target/compiled/ 以下でfindを実行してfileパスを取得 fileパスのcat結果を折りたたみタグ内に格納して文字列に追加 って流れの処理にしています。 このような処理にすることでSQLコンパイル結果のPRコメントを必要分だけコメントできる形としました。また、折りたたみタグに格納することでPullRequestの可視性を損なわないようにしました。 PR上にコメント - name : Comment on PR uses : actions/github-script@v6 with : script : | const fs = require('fs'); const output = fs.readFileSync('compiled_sqls.txt', 'utf8' ); const issue_number = context.payload.pull_request.number; const owner = context.repo.owner; const repo = context.repo.repo; async function processComments() { const comments = await github.rest.issues.listComments( { owner : owner, repo : repo, issue_number : issue_number, } ); const dbtComment = comments.data.find(comment => comment.body.startsWith('DBT Compile Result')); const body = `DBT Compile Result:\n${output}`; if (dbtComment) { await github.rest.issues.updateComment( { owner : owner, repo : repo, comment_id : dbtComment.id, body : body } ); } else { await github.rest.issues.createComment( { owner : owner, repo : repo, issue_number : issue_number, body : body } ); } } PR上のコメントの一覧を取得して、既に DBT Compile Result: で始まるコメントがPR上に存在するのであれば、そのコメントのアップデート、存在しないのであれば新しくコメントをする。 という処理をしています。 条件分岐が発生する処理を簡便に記載したかったのでinlineでjavascriptを記載できる github-script を使用して記述しています これによってactionが走るたびにコメントが新規でされるのではなく、一つのコメントが上書きされ続けるような処理となり、PullRequestの可視性を損なわないようにしました。 どんなふうに動くかみてみる こんなモデルをテストで作ってみました {{ config( schema = ' sample_schema ' )}} SELECT ' hogehoge ' AS name , 30 AS amount UNION ALL SELECT ' fugafuga ' AS name , 50 AS amount {{ config( schema = ' sample_schema ' )}} SELECT SUM (amount) AS sum_amount FROM {{ ref( ' sample_model1 ' ) }} AS sample_model1 以下のようにPullRequest上に変更内容が折り畳まれた状態でコメントされて、ref関数のコンパイル結果がCI環境のデータセットになっていることを確認できます 次にsample_model1.sqlを以下のように修正して再度pushしてみます {{ config( schema = ' sample_schema ' )}} SELECT ' hogehoge ' AS name , 30 AS amount UNION ALL SELECT ' fugafuga ' AS name , 50 AS amount UNION ALL SELECT ' blabla ' AS name , 100 AS amount 以下のように既存のcommentがeditされて、修正後の内容でコンパイルした結果で上書きされていることが確認できます 結果 弊社のデータ基盤は four keys計測による開発ヘルススコアの計測 を行っており、今回の仕組みのリリース前後で開発リードタイムを計測してみましたが、目立った影響は出ていませんでした😢 レビューを円滑にできる環境を整っていないことが課題ではなく、他業務との兼ね合いでDWH開発のレビューに充てることができる時間がそもそも少なそうだったり、レビューに必要なドメインのインプットが足りていないことがボトルネックになっていそうだなという発見にも繋がったので、そこはプラスに捉えています メンバーの声を聞いていると便利なことは間違いないらしいので、レビューコストの低減による持続的なトイル削減に将来的には繋がっていけばいいなと思ってます🙏 使えそうだな。試してみようかなと思っていただけたら幸いです! We’re Hiring! タイミーのデータ統括部では、ともに働くメンバーを募集しています!! product-recruit.timee.co.jp
こんにちは、タイミーのデータ統括部データサイエンス(以下DS)グループ所属の菊地です。 今回は、タイミーがBIツールとして導入しているLookerでの、 H3 を使用した可視化をするための取り組みを紹介したいと思います! H3とは H3 とは、Uber社が開発しているグリッドシステムで、オープンソースとして提供されています。 H3 では、位置情報に紐づいたイベントを階層的な六角形の領域にバケット化することができ、バケット化された単位でデータの集計が可能になります。 タイミーでは、サービスを提供する各都市の需給を測定するために,六角形単位で集計したデータを可視化するなど、様々な場面での分析に活用しており、例えば以下のような可視化を行なっております(数値はランダム値です)。 H3 についての詳細は、以下のページが参考になるかと思います。 https://h3geo.org/ https://github.com/uber/h3 https://www.uber.com/en-JP/blog/h3/ 前提条件 LookerのデータソースはGoogle Cloud BigQueryとします。 可視化に必要な各種ファイルの生成にはPythonを用いており、使用しているPythonバージョンと、依存ライブラリのバージョンについては、以下で動作確認を行っています。 Pythonバージョン: 3.11.4 依存ライブラリ geojson==3.0.1 h3==3.7.6 numpy==1.25.2 pandas==2.0.3 shapely==2.0.1 topojson==1.5 また、大変簡略化した例ですが、データソースのBigQueryプロジェクト・データセット・テーブルは、以下の想定とします。 プロジェクト名: sample-project データセット名: sample-dataset テーブル sales : 売上データを保持しているテーブル sales_at : 売上の日時 amount : 売上 place_id : places テーブルの外部キー places : 位置情報(緯度・経度)を保持しているテーブル places テーブルと sales テーブルには1:nのリレーションが存在 erDiagram places ||--|{ sales: "1:n" places { INTEGER id FLOAT latitude FLOAT longitude } sales { INTEGER id DATETIME sales_at INTEGER amount     INTEGER place_id } Lookerでの可視化を行うための手順 今回は以下の手順に従って、上記saleテーブルの売上をH3六角形にバケット化し、Looker上で可視化します。 緯度経度情報を保持しているBigQueryテーブルにH3六角形IDを付与し、別テーブルとして保存 TopoJsonファイルの作成 作成したTopoJsonファイルをLookerに追加 Lookerのmodelファイルにmap_viewフィールドを追加 Lookerのviewファイルにdimensionを追加 Lookerのmodelファイルにexploreを追加 Lookerでの可視化 1. 緯度経度情報を保持しているBigQueryテーブルにH3六角形IDを付与し、別テーブルとして保存 集計の際に使用する「緯度経度情報を保持しているBigQueryテーブル」(ここでは places テーブル)に対して、H3六角形ID(以下H3 hex idと記載)を付与し、別テーブルとして保存しておきます。ここでは h3_places テーブルとして保存しています。 下記は、 places テーブルをpandas.DataFrameとして読み込み、H3六角形解像度0~15までのH3 hex idを付与し、テーブルとして書き出すコードの例です。 H3六角形解像度は値が大きくなるにつれて、小さな六角形(=解像度が上げる)になり、詳細については下記ドキュメントが参考になるかと思います。 https://h3geo.org/docs/core-library/restable/ import h3 import pandas as pd class BigQueryClient : def __init__ (self): ... def read_table_as_dataframe (self, table_id: str ) -> pd.DataFrame: """BigQueryテーブルをpandas.DataFrameとして読み込む処理""" ... def write_table_from_dataframe (self, df: pd.DataFrame, table_id: str ) -> None : """pandas.DataFrameをBigQueryテーブルを書き込む処理""" ... def make_h3_hex_ids (df: pd.DataFrame) -> pd.DataFrame: _df = df.copy() for resolution in range ( 16 ): # 緯度・経度情報を元に、H3 hex idを付与 _df[f 'h3_hex_id_res_{resolution}' ] = df.apply( lambda x: h3.geo_to_h3(x[ 'latitude' ], x[ 'longitude' ], resolution), axis= 1 ) return _df if __name__ == '__main__' : ... bq_client = BigQueryClient() # 緯度(latitude)、経度(longitude)を保持しているBigQueryテーブルをDataFrameとして読み込む df = bq_client.read_table_as_dataframe( 'sample-project.sample-dataset.places' ) # H3 hex idを付与する h3_df = make_h3_hex_ids(df) h3_df.rename(columns= dict ( id = 'place_id' ), inplace= True ) # H3 hex idを付与したDataframeをBigQueryテーブルとして書き込み bq_client.write_table_from_dataframe(df=h3_df, 'sample-project.sample-dataset.h3_places' ) 例として、以下のような緯度経度を保持しているサンプルデータに、H3 hex idを付与した場合、以下のような結果になります。 import numpy as np np.random.seed( 42 ) tokyo_latitude = 35.6762 tokyo_longitude = 139.6503 df = pd.DataFrame( [[i, np.random.normal(tokyo_latitude, 0.3 ), np.random.normal(tokyo_longitude, 0.3 )] for i in range ( 1 , 11 )], columns=[ 'place_id' , 'latitude' , 'longitude' ] ) h3_df = make_h3_hex_ids(df) h3_df.head( 10 ) 2. TopoJsonファイルの作成 「1. 緯度経度情報を保持しているBigQueryテーブルにH3 hex idを付与する」でH3 hex idを付与したDataFrameを元に、TopoJsonファイルを作成します。 TopoJsonの詳細についてはこちらの「 topojson 」GitHubリポジトリを参照してください。 下記は、TopoJsonファイルを作成するコード例です。 処理の内容としては、GeoJson形式を経由して、TopoJsonに変換し、ファイルとして出力をしています。 TopoJsonファイルは、 H3解像度 別に作成しています。 from pathlib import Path import geojson import h3 import pandas as pd from shapely import geometry import topojson class H3ToGeojson : @ staticmethod def get_h3_geojson_features (h3_hex_ids: list [ str ]) -> list [geojson.Feature]: polygons = h3.h3_set_to_multi_polygon(h3_hex_ids, geo_json= True ) features = [geojson.Feature(geometry=geometry.Polygon(polygon[ 0 ]), properties= dict (h3_hex_id=h3_hex_id)) for polygon, h3_hex_id in zip (polygons, h3_hex_ids)] return features def get_h3_geojson_feature_collection_from_dataframe (self, df: pd.DataFrame, h3_hex_id_column: str ) -> geojson.FeatureCollection: assert df.columns.isin([h3_hex_id_column]).any(), f 'column `{h3_hex_id_column}` is not exists.' unique_h3_hex_ids = df[h3_hex_id_column].unique().tolist() geojson_features = self.get_h3_geojson_features(unique_h3_hex_ids) feature_collection = geojson.FeatureCollection(geojson_features) return feature_collection class H3ToTopojson : def __init__ (self): self.h3_to_geojson = H3ToGeojson() def get_h3_topojson_topology_from_dataframe (self, df: pd.DataFrame, h3_hex_id_column: str ) -> topojson.Topology: feature_collection = self.h3_to_geojson.get_h3_geojson_feature_collection_from_dataframe( df, h3_hex_id_column=h3_hex_id_column ) return topojson.Topology(feature_collection, prequantize= False ) def make_h3_topojson_file_from_dataframe (self, df: pd.DataFrame, h3_hex_id_column: str , save_file_path: Path) -> None : topojson_topology = self.get_h3_topojson_topology_from_dataframe(df=df, h3_hex_id_column=h3_hex_id_column) topojson_topology.to_json(save_file_path) if __name__ == '__main__' : ... h3_to_topojson = H3ToTopojson() save_dir = Path( 'topojson' ) save_dir.mkdir(exist_ok= True ) for resolution in range ( 0 , 16 ): h3_hex_id_column = f 'h3_hex_id_res_{resolution}' h3_to_topojson.make_h3_topojson_file_from_dataframe(df=h3_df, h3_hex_id_column=h3_hex_id_column, save_file_path=save_dir / f '{h3_hex_id_column}.json' ) 例として、先ほど作成したサンプルデータに対して、 resolution=4 を指定してTopoJsonファイルとして書き出す処理は以下のようになります。 h3_to_topojson = H3ToTopojson() h3_to_topojson.make_h3_topojson_file_from_dataframe(h3_df, resolution= 4 ) TopoJsonファイルの中身は以下のようになります。 { " type ":" Topology "," objects ": { " data ": { " geometries ": [{ " properties ": { " h3_hex_id ":" 842f5a3ffffffff " } ," type ":" Polygon "," arcs ": [[ -5 , -2 , 0 ]] ," id ":" feature_0 " } , { " properties ": { " h3_hex_id ":" 842f5bdffffffff " } ," type ":" Polygon "," arcs ": [[ 1 , -4 , 2 ]] ," id ":" feature_1 " } , { " properties ": { " h3_hex_id ":" 842f5abffffffff " } ," type ":" Polygon "," arcs ": [[ 3 , 4 , 5 ]] ," id ":" feature_2 " }] ," type ":" GeometryCollection " }} ," bbox ": [ 139.198358 , 35.267135 , 140.126313 , 36.103519 ] ," arcs ": [[[ 139.44526 , 35.765969 ] , [ 139.458427 , 36.000295 ] , [ 139.695196 , 36.103519 ] , [ 139.918545 , 35.971536 ] , [ 139.903854 , 35.7366 ]] , [[ 139.44526 , 35.765969 ] , [ 139.667342 , 35.634256 ]] , [[ 139.653549 , 35.399825 ] , [ 139.419179 , 35.297723 ] , [ 139.198358 , 35.429167 ] , [ 139.21065 , 35.662982 ] , [ 139.44526 , 35.765969 ]] , [[ 139.653549 , 35.399825 ] , [ 139.667342 , 35.634256 ]] , [[ 139.667342 , 35.634256 ] , [ 139.903854 , 35.7366 ]] , [[ 139.903854 , 35.7366 ] , [ 140.126313 , 35.603627 ] , [ 140.111006 , 35.368594 ] , [ 139.874758 , 35.267135 ] , [ 139.653549 , 35.399825 ]]]} 3. 作成したTopoJsonファイルをLookerに追加 LookerのFileBrowserを開いて、先ほど作成したTopoJsonファイルを追加します。 追加後、適切なフォルダにファイルを移動します。ここでは maps/h3 フォルダにTopoJsonファイルを移動します。 ├── maps    └── h3    ├── h3_hex_id_res_0.topojson    ├── h3_hex_id_res_1.topojson    ├── h3_hex_id_res_2.topojson ...    └── h3_hex_id_res_15.topojson 4. Lookerのmodelファイルにmap_viewフィールドを追加 下記のようにmap_layerを設定します。map_layerは H3解像度 別に設定しています。 property_key は「2. TopoJsonファイルの作成」で使用している H3ToGeojson.get_h3_geojson_features メソッド内の geojson.Feature の引数で設定している properties のkey名である h3_hex_id を指定しています。 map_layer: h3_hex_id_res_0 { file : "/ maps / h3 /h3_hex_id_res_0. topojson " format : topojson property_key: " h3_hex_id " } map_layer: h3_hex_id_res_1 { file : "/ maps / h3 /h3_hex_id_res_1. topojson " format : topojson property_key: " h3_hex_id " } map_layer: h3_hex_id_res_2 { file : "/ maps / h3 /h3_hex_id_res_2. topojson " format : topojson property_key: " h3_hex_id " } ... map_layer: h3_hex_id_res_15 { file : "/ maps / h3 /h3_hex_id_res_15. topojson " format : topojson property_key: " h3_hex_id " } 5 . Lookerのviewファイルにdimensionを追加 map_layer_nameは、「4. Lookerのmodelファイルにmap_viewフィールドを追記」で作成した、map_layer名を指定します。 dimensionは H3解像度 別に設定しています。 view : h3_places { sql_table_name: ` sample - project . sample - dataset .h3_places` ;; dimension : h3_hex_id_res_0 { group_label: " H3 " group_item_label: " H3 解像度0の六角形 ID " type : string sql : $ { TABLE } .h3_hex_id_res_0 ;; map_layer_name: h3_hex_id_res_0 } dimension : h3_hex_id_res_1 { group_label: " H3 " group_item_label: " H3 解像度1の六角形 ID " type : string sql : $ { TABLE } .h3_hex_id_res_1 ;; map_layer_name: h3_hex_id_res_1 } dimension : h3_hex_id_res_2 { group_label: " H3 " group_item_label: " H3 解像度2の六角形 ID " type : string sql : $ { TABLE } .h3_hex_id_res_2 ;; map_layer_name: h3_hex_id_res_2 } ... dimension : h3_hex_id_res_15 { group_label: " H3 " group_item_label: " H3 解像度15の六角形 ID " type : string sql : $ { TABLE } .h3_hex_id_res_15 ;; map_layer_name: h3_hex_id_res_15 } } 6 Lookerのmodelファイルにexploreを追加 下記のようにexploreを追加します。 explore : sales { label : " sales " ... join : h3_places { view_label: " place " type : inner sql_on: $ { sales .place_id } = $ { h3_places.place_id } ;; relationship : many_to_one } } 7. Lookerでの可視化 作成したexploreでマップでの可視化を行うと、地図上にH3六角形メッシュが表示され、メッシュ毎にバケット化された集計値を色で表現することができます。 下記は東京近郊のデータを H3解像度 7のdimensionを使用して可視化した例です(数値はランダム値です)。 今回作成した H3解像度 dimensionを変更することで、目的に合わせて六角形メッシュの大きさを変更して可視化を行うことが可能です。 まとめ 今回は、Uber社がオープンソースとして提供している H3 を使用して、Looker上で可視化を行う方法について解説しました。 タイミーでは今回紹介したLookerでの可視化以外にも、機械学習の特徴量作成時に使用するなど、様々な場面で H3 を活用しています。 今後も地理情報を活かした分析をする際に活用していきたいと考えています。 We’re Hiring! タイミーのデータ統括部では、ともに働くメンバーを募集しています!! 現在募集中のポジションは こちら です! 「話を聞きたい」と思われた方は、是非一度カジュアル面談でお話ししましょう! mermaid.initialize({startOnLoad: true});
はじめに マッチング領域、ワーキングリレーションチームの @Juju_62q です。 タイミーでは有志のメンバーが集まって1年半ほど前から輪読会を行っています。 現在5冊の書籍を読み終わっています。 ブログのタイトルにもありますが、今回 エンタープライズ アプリケーション アーキテクチャパターン (略称PoEAA)の輪読を実施しました。 書籍選定には以下のような狙いを置いていました。 複雑な ドメイン をソフトウェアで扱うための術を身につける アクティブレコードパターンにより詳しくなり、 Rails を上手く扱えるようになる 書籍に出てくる デザインパターン をきっかけとしてタイミーの アーキテクチャ について議論し、実験のきっかけを作る。 輪読会の方法 週1でmiroを利用して実施しています。 輪読会は参加者の「わからなかったこと」、「話してみたいこと」を掘り下げるような形で実施しています。 また、特別取り上げることは少ないですが、個人の感想や勉強になった場所を書けるようになっています。 「わからなかったこと」では参加者が書籍を読んでわからなかったことについて話します。 これは少し理解が曖昧な部分について、みんなで解説を行い理解を深めている様子です。 「話してみたいこと」ではタイミーの話や、ソフトウェアエンジニアリング一般で用途や使い分けについて話しています。 下記は「重ロック」の概念をタイミーのマッチングロジックに適用できないか話している様子です。 感想はそれぞれが自由に書いています。 輪読会では「参加者が少しでも参加前より理解できること」を目的に以下のような心構えを置いています。 本輪読会は読んだ人の疑問や議論したいことに基づいて意見交換をする形をとっています。したがって、参加者の協力がなければ良いものになることはありません。チームの学びを最大化するためにもあなたの思ったことを教えてください。 発言の正しさや凄さに関わらず、疑問、自分の意見を発言することに対して常にポジティブです。 ファシリテーター 及び参加者の皆さんはどんな意見も歓迎し、発言したことにまず感謝をするように努めてください。 特に今回は解釈が難しい書籍だったと思うので、「わからない」が主張しやすいことは大事だったと思っています。今後も参加者の「わからない」を大事にして運用していきたいと考えています。 学びや感想 岡野 Web Application Frameworkがよくできているというのを強く感じることができる書籍でした。 書籍の中に出てくる デザインパターン や考え方はタイミーでも実際に活かせることが多く、具体のテーブルやモデルについて議論ができるのはとても良い体験だったと思っています。 ActiveRecord と ドメイン モデルの関係とかの話が2002年の段階で整理されていたのは本当にすごいです。 難波 タイミーのバックエンドは Ruby on Rails を使っているので、この本を読むことでActive Recordの複雑さや大変さについて認識を新たにすることができたのはとても良かったです。書籍が発行された時代を感じるような課題意識や制約も多々ありますが抽象的に捉えることで現代でも参考になる部分はたくさんあったので、こういった過去の名著を一通り読んでみるのはオススメです。それにしてもLockは考えることが多くて本当に大変。 須貝 いつかは読みたいと思いながらなかなか読めずにいたPoEAAにチャレンジできて本当に良かったです。機会を作ってくれた輪読会主催メンバーに感謝しています。 書籍の中で紹介されているパターンが開発者間での共通言語となり「これは重オフラ インロック では?」といった会話がSlack上で発生することもありました。自分としてはまだまだ理解できていない部分もたくさんあるので今後も読み返していきたいと思います。 texta.fmの最初のほうのエピソードを聴くと Ruby on Rails との関係性も含めてさらに理解が深まるので本当におすすめです。自分はかなり助けられました。 終わりに PoEAAは2002年とそれなりに古い書籍ではありますが、考え方は現代にも通じる良い本です。 ここに書かれている設計パターンは Rails でもたくさん実装されています。 複雑なビジネスを扱う上で強力な武器になると思うので、みなさんぜひ一度読んでみてください。