「自社サービス開発には明確な答えなどない。自ら解を創り上げていくのが面白いんです」【askenエンジニア語録 vol.1】

こんにちは!
asken人事兼ブログ編集担当の平賀です。
どんな人があすけんの仕事を楽しんでいるかをここでもご紹介したい!
ということで、あすけんで現在サーバーサイドエンジニアとして活躍している羽鳥さんに、直球インタビュー。エンジニア目線で見た時のaskenの仕事について、いろいろ聞いてみました!

<プロフィール>

f:id:techaskeninc:20220114190308p:plain

◆ 羽鳥さん(2021年5月入社)
◆ 好きな食べ物:カレー、餃子、魚介類全般
◆ 趣味 :インドアでは「ボードゲーム、サウナ」、アウトドアでは「登山 初心者 、散歩」
◆ 性格を一言で:「明るく、楽しく、前向きに」な ATM( 笑 ) な性格

自社サービスの開発に携わりたかった。

ーまずは羽鳥さんのキャリアについて教えてください。

大学卒業後、新卒で中堅のシステム開発会社へ入社しました。
そこではサーバーサイドエンジニアとして、クライアントのシステム開発に約7年間従事。
エンタメ系情報サイトの開発・運用や、医療従事者向けのPCアプリケーションの開発、プッシュ通知機能の開発、エネルギー系企業のtoC向けサービス開発などに携わりました。
様々なシステム開発に関われたことはとてもいい経験だったと思います。後半はプロジェクトリーダーとしても経験を積むことができましたね。

ーでは、羽鳥さんがaskenに入社を決めた理由は?

大きく2つあります。

1つ目は、自社サービスの開発に関わりたかったから。
それまではシステムの受託開発だったので、そうではない世界で経験を積んでみたいと思っていました。

2つ目は、せっかくなら社会的意義の大きい仕事、例えば社会課題の解決につながるようなビジネス領域で貢献したいという想いがあったのですが、askenならそれが叶えられると思ったからです。
転職を考えた時期はコロナなどの影響もあり、自分自身の健康を見つめ直すタイミングとも重なりました。
もともと妻が食事管理アプリ『あすけん』を使っていたこともあって、「asken=健康」というイメージを強く持っていました。

アプリを知れば知るほど「このプロダクトに関わりたい」という思いが強くなっていったんです。

自分ですべて決められる楽しさと苦しみ。

ー入社後はどんな自社サービスの開発に携わることになりましたか?

現在は、サーバーサイドエンジニアとして、糖尿病治療用アプリの開発に携わっています。
これは、すでに多くの方にご利用いただいている食事管理アプリ『あすけん』の技術やそこで培ったノウハウを活かし、糖尿病の栄養食事指導を補助するアプリです。

糖尿病の患者さんは、毎日の食事をきちんと記録する必要がありますが、毎食これを続けるのは手間もかかります。
毎日の食事をスマートフォンで写真撮影するだけで食事記録ができるようになれば、患者さんの負担を大きく減らすことができます。

さらに、「いつ、何を、どれだけ食べているか」をこのアプリで正確に把握することで、病院の管理栄養士が患者さんへ栄養食事指導を行う際のサポートも可能になります。

エンジニアチームだけで開発を進めるのではなく、プロダクトオーナーや医療業界出身のさまざまな職種のメンバーと共に、仕様やUI/UX設計なども含めてプロダクト全体を見ながらプロジェクトを進めています。

ーふむふむ。実際に自社サービスの開発をしてみてどうです? 仕事楽しいですか?

楽しいですw

受託開発をしていた頃は、要件が下りてきて、開発して、と流れや答えがありましたが、今は自分たちで答えを見つけないといけない。これは本当に面白い作業です。
答えが明確にあるのも取り組み甲斐があるものですが、答えがない中でみんなであれこれ考えたり、答えを創り上げていくステップはとても楽しいですね。

ーなるほど。自社サービス開発の醍醐味は、やっぱり自分たちで決めて進めていける点なんですね〜。

まあ、そうです。が、実際にやってみると、自分が思い描いていたものとはギャップがあることにも気付きました。

受託側にいた前職までは、「自社側で開発ができれば、自分たち主導でいろいろ決められる分、やりやすいはず」と思っていました。もちろん、実際にそういった場面はとても多く、イメージ通りの部分もあります。

ただ、一方で「自分たちで決断しなければならない分、大変さもある」ということがわかってきました。

新しいプロダクトを生み出すためには、何も決まっていない中で自分たちで解を見つけていかなければならない。この苦しみは実際に経験してみるまで分かりませんでした。でも私にとって、これは意味ある試練だと思ってますし、本当にいい経験になっています。

何より、自分が携わったサービスが世に出て多くの人に使ってもらえるようになるのはとても楽しいですし、サービスを通してユーザーさんの健康のサポートができることは、とてもやりがいが大きいです。

責任感や使命感がむしろモチベーション。

ーいいことばっかり聞いてきてしまいましたが、逆に仕事で大変だったことは?

糖尿病治療用アプリの開発は、やっぱり大変でしたよ。

askenはアプリの開発はしていますが、医療機器メーカーではありません。エンジニアチームのメンバーはもちろん医療機器分野の開発に通じているわけではありませんでした。 当たり前ですが、業界用語や医療分野特有の規定なども知識が全くないところからのスタートでしたし、そこを理解しながら開発を進めていくのはとてもハードでした。

ー確かに、これまでのtoC向けのアプリ開発とは全く異なるものですもんね。そんな中、モチベーションを保ち続けられたのはなぜですか?

好奇心ですねw

開発を進める中で、いろいろな栄養素を知ったり、それについてまた勉強したりということが多々あるのですが、もともと知らないことを学習していくことが好きですし、さらに健康に対する意識が高まりました。今後もさらに知識を深めていきたいと思ってます。

患者さんや医療機関の方々が使うものなので、しっかりとした高い品質を約束していくという責任感や使命感がむしろモチベーションの源泉になっています。

ー社会的意義の大きいサービスだからこそ責任も重大ですよね。そんな中、課題に感じていることはありますか?

入社する前までは、あすけんのいちユーザーとしての視点しか持っていませんでしたが、自ら開発に関わることで裏側を知り、改めて見えてきた課題がたくさんあります。

まさに今、システム部一丸となってシステム内のブラックボックス見える化し、課題を抽出して改善に取り組んでいるところです。これは一つ一つ地道に続けていきたいですね。

また、エンジニアチームは皆優しくてマイルドなメンバーが多いのですが、時にはぶつかることもしながらいいものを作っていけるチームにしていきたいなぁと思ってます。

ーちなみに、一緒に働いているエンジニアたちってどんな人たちですか?

選考時に会った、エンジニアリングマネージャーの服巻さんやマネージャーのおすぎさんの印象は入社前も今も変わらないですね!

2人とも裏表がなく、服巻さんは明るくポジティブで、おすぎさんは柔らかな感じ。入社後、ギャップを感じたとすれば、おすぎさんが「(´・ω・`)」を多用する人だったことくらいですw

医療チームをはじめ、システム部はとてもアットホームな雰囲気ですね。
医療チームにはiOSエンジニア、Androidエンジニア、サーバーサイドエンジニアが1~2名所属し、皆でプロダクト開発に取り組んでいます。
メンバーそれぞれ専門分野の知識や経験が豊富で、キャッチアップした情報を共有していく風土があるのでとても刺激的です。それが良いプレッシャーにもなっています。

業務のやりとりなどはSlackで行うことが多いですが、わりと頻繁に音声での会話もするし、メンバー同士のコミュニケーションは取りやすく、みんなウェルカムな雰囲気で社交的ですね。
私個人は、Slackなどのツールの中で何かとつぶやいてみるタイプなんですが、askenメンバーも同じように自由に発信してる人が多くて楽しいです!

ーそんなチームの中で、羽鳥さんがエンジニアとして今後チャレンジしていきたいと思ってることは?

askenには医療事業のほかに、コンシューマ事業、法人事業、海外事業と複数の事業がありますが、エンジニアは社内に15名前後と限られています。

だからこそ、自分自身の技術領域を越えて幅を広げていく必要があります。私は、今まではサーバーサイドメインだったけど、インフラとかも勉強して、少数精鋭のこのチームの中で貢献できることを増やしていきたいと思っています。

また、自分たちでジャッジして物事を決めていくことが多いので、もっと勉強してチームビルディングやマネジメントなどもできるように成長していきたいですね。

ー羽鳥さん、ありがとうございました! 最後に、askenに興味を持ってくれたエンジニアさんにお勧めしたいポイントはありますか?

askenには、チャレンジできる土壌・風土があります。

大小に関わらず、自分がやってみたいと思ったことをメンバーや社内に提案すると、「やってみよう!」と前向きに捉え、チャレンジさせてもらえるところが魅力だと思います。
私はまだ入社半年強ですが、2022年からは医療チームのプロジェクトマネジャーとして新しい役割にチャレンジし始めました。年齢や社歴などに関わらず、いろいろなチャンスに出会える会社だと実感しています!

f:id:techaskeninc:20220114190744j:plain
2022年の初日の入りと共に~

現在、askenではエンジニア大大大募集中!

羽鳥さんのように、少数精鋭チームの中で領域を超えて活躍したいと考えている意欲あるエンジニアの方、ぜひともaskenで力試ししてみませんか?
カジュアル面談も随時行っていますので、是非お声がけください^^

次回は、Androidアプリ開発メンバーに仕事の楽しみを根掘り葉掘り聞いてみる予定です。
あすけんエンジニアチームに興味のある方はお見逃しなく!

www.wantedly.com

www.asken.inc

Firebaseイベント、BigQuery、Google Data Studioを利用したABテストの可視化

f:id:techaskeninc:20211208103108p:plain

はじめに

はじめまして!

askenで北米版アプリのサーバーサイド開発を担当している大倉と言います。サーバーサイド開発の他、データの抽出や可視化も担当しています。

北米版では全ての施策に対してABテストを行い、その結果を元に意思決定をしています。FirebaseのコンソールやGoogle Analyticsを使えば、コンバージョン率などの簡単な指標の確認はできます。しかし、コンバージョン率だけではなく、より細かい指標を分析したい場合は、独自でFirebaseのイベントログを解析し、分析する必要があります。北米版あすけんでは、上に添付している画像のレポートを利用して、ABテストの結果を可視化・分析し、意思決定に繋げています。(一部の情報は黒塗りで表示しています。)

今回は、北米版あすけんで取り組んでいるABテストの結果を可視化する方法をご紹介したいと思います。

対象読者

  • 基本的なクエリが書ける人
  • FirebaseのコンソールやGoogle Analyticsを用いたABテスト分析では物足りない人
  • ABテストをより細かく分析して意思決定の材料にしたい人

Firebase, BigQuery, Google Data Studioを使ったABテスト結果の可視化

Firebaseイベントとして記録するべき情報

北米版あすけんでは、ユーザーの行動ログをFirebaseのイベントとして記録しています。新しい機能を実装したら、基本的には以下の3つのイベントをアプリに新しく仕込んでいます。

  1. pvイベント
  2. tapイベント
  3. actionイベント

pvイベントとは、あるページを開いた際に発火するイベントです。ユーザーがあるページに遷移したかどうかを知りたいときなどに利用できるイベントです。

tapイベントとは、ボタンやトグルなどアプリ上の押下可能なコンポーネントが押下された際に発火するイベントです。新しく追加した機能がユーザーに利用されているかどうかなどを知りたい時に使えるイベントです。

最後にactionイベントとは、ある機能を利用してアクションを起こした際に発火するイベントです。あすけんで例えると、食事を登録した時、食事を編集した時などにactionイベントを発火させています。ある機能に対するコンバージョン率などを調べたい際に利用できるイベントです。

これらのイベントを実装したら、次はBigQueryを使って、欲しいデータに加工します。

BigQueryを使ってFirebaseのイベントログを加工する

今度は、BigQueryを使ってFirebaseから送られてきたイベントログを可視化するのに必要な形に整えます。ABテストの結果として知りたい情報は、基本的には以下の二つです。

  1. イベント発火率
  2. イベント発火数

イベントの発火率は、ある機能やページがどれくらいのユーザーに利用されたのかを把握する際に使用します。

一方、イベント発火数はある機能やページがユーザーによって何回利用されたのかということを把握する際に利用することができます。

それでは実際にクエリを書いてみたいと思います。以下の手順でクエリを書きます。

  1. 対象イベントログの生ログを確認する。
  2. ユーザーごとにイベント発火率とイベント発火数の情報を持つデータを抽出する。
  3. 上記データに対してABテストの情報を付与したデータを抽出する。

まず、情報を抽出したい対象のイベントログにどのような情報が含まれているのかを確認します。

北米版あすけんには ユーザーが食事登録を完了した際に発火するlogged_foodというactionイベントがあります。これを例にデータを抽出してみたいと思います。

SELECT
    *
FROM
    `{dataset_name}.events_*` as e
WHERE
    -- 生ログの確認がしたい時などは_TABLE_SUFFIXにを日付を指定してあげるとクエリのデータ量の節約になる。
    (_TABLE_SUFFIX = '20210101')
    -- イベント名を指定する
    AND event_name = "logged_food"
LIMIT 1

結果は以下です。 f:id:techaskeninc:20211208103203p:plain

Firebaseのイベントログには、デフォルトで記録される情報と独自で記録した情報が含まれます。独自で記録したパラメータについて簡単に紹介したいと思います。 logged_foodの例で言えば、event_params.keytypeに記録されている値(event_params.value.string_value)が独自で記録した情報になります。こちらのパラメータの情報を使うことで、どの機能を経由して食事が登録されたかということを識別することができます。例えば、北米版あすけんにはテキスト検索や履歴や画像検索など、食事を登録する複数の方法があります。これらの機能を識別するための情報がevent_params="type"というパラメータに含まれています。イベントログに独自のパラメータ情報を持たせると、より細かい分析が可能になります。こちらのパラメータの情報を用いて使われた機能を識別する方法は次で詳しく説明したいと思います。

次にユーザーごとにlogged_foodを発火したか(食事登録をしたかどうか)というフラグと、何回発火したか(何回食事を登録したか)という情報を取得したいと思います。

SELECT
    user_id,
    -- ユーザーの1回のアクションに対して複数回イベントが発火する場合が存在する。それらのログを排除するため、event_timestamp(イベントが発火した時間)がユニークなログをカウントするようにしている。
    COUNT(DISTINCT IF (event_name = "logged_food", event_timestamp, null)) as num_logged_food
    -- 一人のユーザーが指定しているイベントを発火しているかどうかを1, 0で表す。この値の平均値をとるとイベントの発火率がわかる。平均値はData Studioの機能で算出する。
    , MAX(IF (event_name = "logged_food", 1, 0)) as flg_logged_food
    , MAX(IF (event_name = "logged_food", ep.key = "type", ep.value.string_value = "text", 1, 0)) as flg_logged_food_text
    , MAX(IF (event_name = "logged_food", ep.key = "type", ep.value.string_value = "history", 1, 0)) as flg_logged_food_history
FROM
    `{dataset_name}.events_*` as e
    , unnest(`event_params`) as ep
WHERE
    -- テスト実行したい時などは、_TABLE_SUFFIXに日付を指定してあげるとクエリのデータ量の節約になる。
    (_TABLE_SUFFIX = "20210101")
GROUP BY
     user_id

結果

f:id:techaskeninc:20211208103219p:plain

ここで注意するべきポイントは、イベント発火率・イベント発火数の出し方です。

イベントの名称の指定はMAX(IF (event_name = "logged_food", 1, 0))のようにIF文の中でしています。こうすることで、他のイベント情報も並列で取得することができるようになります。例えば、delete_foodというイベントがあったとしたら、MAX(IF (event_name = "delete_food", 1, 0))というカラムを増やすだけで他のイベント情報を追加することができます。where文でevent_nameの指定をする必要はありません。

イベントの発火率を出すため、ユーザーごとに発火したかどうかというフラグ情報を持たせます。 この情報をフラグとして持たせる理由は、全てのユーザーの当フラグの平均値をとると、ある機能がどれくらいの割合のユーザーによって利用されたのかを知ることができるからです。平均値はData Studioの機能を使って算出するため、BigQueryでデータを加工する際には、ユーザーごとにフラグを持たせるところまでを行います。Data Studioの機能を使ってフラグの平均値をとる方法は後ほど詳しく説明します。

イベント発火数は、ユーザーに紐づくイベントの数を合計することで取得することができます。ただし、ここで注意点がひとつあります。イベントが一度発火すると、複数のログが残るケースが見られます。それらの重複ログを排除するため、event_timestamp(=イベントが発火した時間)がユニークなものだけをカウントします。そうすることでより正確なイベント発火数を取得することができます。

どの機能を経由して食事が登録されたかという情報を識別したい場合どうすれば良いかもここで説明します。上述したように、event_params.keytypeに記録されている情報を使うことで、どの機能を経由して食事が登録されたかということを識別することができます。

Firebaseのイベントは情報がネスト化して記録されているので、必要な情報を引っ張ってくる際には、ネストを解除してあげる必要があります。ネストの解除はunnest()を使って行います。

event_paramsをunnestした情報を使ったのが、select文の6行目と7行目になります。 このように、IF文の中でkeyとvalueを指定することができます。where文で指定することもできますが、そうしない理由としては、where文でunnestしたevent_paramsを指定すると、unnestしたevent_paramsを使いまわすことができなくなるからです。例えば、text経由とhistory経由の情報をそれぞれ取得したとなった場合、where文でevent_paramsの値にtextを指定してしまうとhistoryの情報は取得できなくなってしまいます。しかし、IF文で条件指定をしてあげればunnestしたevent_paramsの値を固定することなく、使いまわすことができます。

最後に、上記データに対してABテストの情報を付与したデータを抽出します。 北米版あすけんでは、ABテストの振り分けはモバイル側で行い、その振り分けの結果をサーバーに保存しています。したがって、DBに保存されている振り分け結果を先ほど抽出した結果に付け加えます。ABテストの情報を付与する理由としては、Data Studioのフィルタリング機能を使ってABテストごとの結果を見れるようにするためです。

ABテストのテーブルの構成は以下のようになっています。

id       INTEGER
user_id  INTEGER
name     STRING
pattern  STRING
created  STRING
modified STRING
SELECT
    e.user_id
    , ab.name
    , pattern
    -- ユーザーの1回のアクションに対して複数回イベントが発火する場合が存在する。それらのログを排除するため、event_timestamp(イベントが発火した時間)がユニークなログをカウントするようにしている。
    , COUNT(DISTINCT IF (event_name = "logged_food", event_timestamp, null)) as num_logged_food
    -- 一人のユーザーが指定しているイベントを発火しているかどうかを1, 0で表す。この値の平均値をとるとイベントの発火率がわかる。平均値はData Studioの機能で算出する。
    , MAX(IF (event_name = "logged_food", 1, 0)) as flg_logged_food
    , MAX(IF (event_name = "logged_food" and ep.key = "type" and ep.value.string_value = "text", 1, 0)) as flg_logged_food_text
    , MAX(IF (event_name = "logged_food" and ep.key = "type" and ep.value.string_value = "history", 1, 0)) as flg_logged_food_history
FROM
    `{dataset_name}.events_*` as e
    , unnest(`event_params`) as ep
LEFT JOIN a_b_testing as ab USING(user_id)
WHERE
    (_TABLE_SUFFIX = "20210101")
GROUP BY
     user_id
     , ab.name
     , pattern

結果

f:id:techaskeninc:20211208103236p:plain

ユーザーによっては複数のABテストに割り当てられているため、ユーザーのイベント発火率・発火数の情報は割り当てられたABテストの数だけ複製されることになります。こうすることでData Studioのフィルタリング機能を使って、分析したABテストを選択し、そのABテストの結果のみを見れるようになります。ABテストをレポート化することで、クエリを書けないメンバーであっても誰でもABテストの結果にアクセスし分析することが可能になるというメリットがあります。

それではこの結果を使って、Data Studioで結果を可視化したいと思います。

Data StudioでABテストの結果を可視化する

以下の手順でABテストの可視化を行います。

  1. レポートを作成し、データソースを追加する。
  2. レポートにABテストのフィルタリング機能を追加する。
  3. グラフを追加し、イベントの発火率・発火数を確認できるようにする。

まず、新規のレポートを作成し、作成したクエリを利用してデータソースを作成します。データを追加/カスタムクエリページに遷移し、カスタムクエリ入力欄に、BigQueryで作成したクエリを入力し、追加します。

f:id:techaskeninc:20211208103256p:plain

次に、ABテストの名前でフィルタリングをかけるためのコントロールを追加します。コントロールを追加/プルダウンリストを選択し、コントロールフィールドにABテスト名を値としてもつカラムを選択します 。今回の例では、nameカラムがそれに該当します。こちらのコントローラーを利用することで、ABテストごとにフィルターをかけることができるようになります。

f:id:techaskeninc:20211208103316p:plain f:id:techaskeninc:20211208103321p:plain

最後に、発火率・発火数が確認できるグラフを追加し、ABテストのバリアント(テストパターン)間の比較ができるようにします。

棒グラフを追加し、ディメンションにバリアント情報を持つカラムを指定します。今回の例でいうと、patternがそれに該当します。そして、指標にイベントを発火したかどうかを表すフラグを値として持つカラムを指定します。今回はflg_logged_foodがそれに該当します。

グラフが追加できたら、どれくらいのユーザーがそのイベントを発火したのかを知るための数値を算出・表示します。デフォルト設定では、指定した指標の値が"合計"になっているので、これを"平均値"に変更します。平均値に変更すると0.5などの数値を取得することができます。これをパーセント表記にするため、指標のタイプを数値/%に変更します。パーセント表記にすることで直感的な比較が可能になります。

f:id:techaskeninc:20211208103341p:plain

今のままだと割合がグラフ上に表示されないため、グラフにカーソルを持っていく必要があり手間がかかります。そのため、グラフ上に数値を表示させるための修正をします。グラフのスタイルページにて、"データラベルを表示"にチェックをつけます。すると、バリアントごとのコンバージョン率がそれぞれグラフ上に表示されるようになります。

f:id:techaskeninc:20211208103402p:plain

他の指標もそれぞれ同じ要領で追加します。発火回数も同じように平均値で表示します。

f:id:techaskeninc:20211208103416p:plain

以上でABテストの結果の可視化ができました。あとはフィルターの値を確認したいABテスト名に変更し、バリアント間の数値を比較します。

まとめ

今回は、北米版askenで取り組んでいるABテストの結果を可視化する方法についてご紹介しました。

今回は、一部のイベントログのみを利用しましたが、出来るだけ多くのログをData Studioのレポートに取り込むことで、ABテストの結果をもれなく分析することができるようになります。北米版あすけんでは、アプリのページごとのレポートを作成し様々な指標を盛り込んで、細かくABテストの結果が分析できるようにしています。

基本的な作り方はこの記事の通りですので、皆さんもぜひ作ってみてください。

積極採用中です!

askenでは、一緒に働いてくれるエンジニアを募集しています。少しでも興味を持っていただけたら、ぜひ採用情報をご確認ください。 www.wantedly.com

iOSでGoogle Analyticsへのイベント送信の実装と確認作業を効率化した話

こんにちは。システム部の大澤です。 普段は北米版あすけんのiOSアプリを開発しています。 今回はGoogle Analyticsに送信する行動ログのイベントの実装を効率化したことについてまとめました。

Google Analyticsとは

Google Analyticsはアプリの使用状況とユーザーエンゲージメントについて分析できる、無料のアプリ測定ソリューションです。 Google Analyticsは元々、WebのツールであったがFirebase Analyticsと統合されて、アプリも計測できるようになりました。 北米版あすけんでは行動ログのイベントを計測するためにFirebaseのSDKを使って、Google Analyticsに送信しています。

行動ログとは

行動ログとは、ボタンのタップイベントや画面のPVなどがあります。 Firebaseを使うことで初期設定でもある程度の行動ログを計測できます。

細かいデータを分析するには独自に実装することが必要です。 Swiftでの実装例として、ボタンのタップイベントの下記のようになります。

import Firebase

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        title = "Test"
    }
    @IBAction func buttonTapped(_ sender: Any) {
        Analytics.logEvent("tap_button_event", parameters: [
            AnalyticsParameterItemID: "id-\(title!)",
            AnalyticsParameterItemName: title!
        ])
    }
}

移行前の実装フロー

f:id:techaskeninc:20211203121820p:plain

  • 実装する人はGoogleスプレッドシートに追加、変更するイベントを追加。
    • 主にドキュメントとして使用していた。
  • 追加したイベントをiOSのプロジェクトで実装する。

課題

  • Googleスプレッドシートに定義されたものと同じものが実装される保証はない。
    • スペルミスやコピーミスで意図しない実装になる可能性がある。
  • 差分を追いづらい。
  • バージョン管理の仕組みがない。

変更後のフロー

f:id:techaskeninc:20211203121935p:plain

  1. 行動ログの実装で必要な情報はGitHubで管理するように変更。
  2. YAMLで実装するイベントを定義する。
  3. YAMLの定義の仕方は移行前のGoogleスプレッドシートと同じにした。
  4. スクリプトを使って、YAML -> CSVに変換して、ドキュメントとして利用する。(エンジニア以外の人も確認を容易にするため)
  5. このタイミングでSpell checker、重複チェック、文字列の長さのチェックを行っている。収集と設定の上限
  6. YAML -> JSONに変換し、JSONを元にして、Swiftのクラスを生成する。
  7. quicktypeを使うことでJSON -> Swiftへ変換が容易にできます。
  8. 実装時に関係ないデータはこのタイミングで消している。
  9. JSONを元に変換されたSwiftのクラスをSwift Package Managerを使って、配布する。
  10. 実装するプロジェクトでSwift Package Managerを使って追加する。それによって、プロジェクトで生成したSwiftのクラスの呼び出しが可能になる。

ここでは上記で作成したものをEventTrackerというフレームワークにして、Swift Package Managerで追加しました。 例として、下記のような実装になります。

import Firebase
import EventTracker

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        title = "Test"
    }
    @IBAction func buttonTapped(_ sender: Any) {
        let result = EventTrackStore.shared.apply(request: FirebaseEvent.Get(keyPath: \.tapButtonEvent))
        Analytics.logEvent(result.eventName, parameters: [
            result.params.key.analyticsParameterItemID: "id-\(title!)",
            result.params.key.analyticsParameterItemName: title!
        ])
    }
}

解決した課題

  • スペルミスやコピーミスで意図しない実装になる可能性がある。
    • スクリプトやツールを使い変換するのでドキュメントと実装するコードが一致する。
  • 差分を追いづらい。
    • GitHubで管理するので差分が追いやすくなる。
  • バージョン管理の仕組みがない。
    • Swift Package Managerで管理するのでバージョン管理が容易になる。

動作確認の改善

  • 実装完了後、意図通りにイベントが送信されているかを確認する必要があります。

改善前

  • 確認方法として、Firebaseの管理画面で確認する。(Debug Viewなどを使う)
  • Xcodeのログで送信されているかエンジニアが確認する。
  • 上記のともにアプリ内で完結できず、作業に手間がかかります。

改善策

  • エンジニア以外の方にも確認を容易にするため、デバッグメニューで送信したデータを見れるようにしました。
    • SwiftのコードでAnalytics.logEventを呼び出す前にデバッグメニューのクラスにそのデータを渡すように実装した。

f:id:techaskeninc:20211202113934p:plain:w320

  • デバッグメニューで実装したものが意図通りに動いているか、エンジニア以外の方に確認してもらえます。
    • 前提として、イベントがFirebaseに正常に送信されていることを確認済み。

今後の展開

  • Androidに同じ仕組みを展開する。
  • 未使用のイベントを検知する。
    • 未使用のイベントはムダなデータとなるため、検知できるように仕組み化して今後対応していきたいです。

まとめ

行動ログのイベント実装はサービスを成長させるために必要であり正確な実装が要求されます。 また、正確な行動ログの収集には、機械的にミスが入り込まない仕組みにしていくことが重要だと思います。 今後も継続的に改善していきたいです。

積極採用中です!

askenでは、一緒に働いてくれるiOSエンジニアを募集しています。少しでも興味を持っていただけたら、ぜひ採用情報をご確認ください。 www.wantedly.com

募集職種 — 株式会社asken (あすけん)www.asken.inc

参考記事

スクラムチームの見える化にAsanaを活用した話

こんにちは。システム部の @nakawai です。

普段は北米版あすけんのAndroidアプリ開発と、スクラムマスターを兼任しています。

弊チームでは現在、ユーザー価値を最大化するために仮説検証サイクルの様々な改善に取り組んでいます。

この記事では、その中のひとつであるチケット管理方式の改善に取り組んだ事例を紹介します。

SaaS型ワークマネジメントツール Asana を活用することによってプロダクトやスクラムチームの見える化が進み、PO(プロダクトオーナー)と開発チーム間の情報共有を改善することができました。

以前のチケット管理方式と、その課題

f:id:techaskeninc:20211104162821p:plain

施策チケット管理はAsanaを利用していました(2020年にRedmineから移行)。

一方、開発チームはGitHubのProjectでチケット管理をしていました。この方式において、以下のような課題がありました。

  1. 施策チケットと開発チケットを相互に辿れず、情報を把握しづらい
  2. 開発チケットの、親子タスクや依存タスクの管理が辛い
  3. 開発チケットのStory Pointの集計が手間
  4. 時系列での可視化がしづらい

これらの課題1を解決するため、実用性の検証を経て以下のようなチケット管理方式に移行しました。

現在のチケット管理方式

f:id:techaskeninc:20211104162952p:plain

これによって、以下のようなことが実現できました。

施策に必要なタスクをすべてサブタスクで紐付け

開発だけでなく、デザインやABテストなどすべてのタスクや、さらにそのサブタスクをすべてチケット階層で紐付けられるようになりました。別途、ワークフロー自動化ソリューションの UnitoGitHubとAsanaを同期させることにより、PR(プルリクエスト)がマージされたらAsanaの開発チケットをDoneにするといったことも実現できました。

チケット同士の依存関係を管理

リリースのマイルストーンとそれに必要なタスクを紐付けたり、アプリ開発着手前に完了していてほしいAPI開発のタスクなどを紐付けることで、リリースまでのクリティカルパスを可視化できます。 f:id:techaskeninc:20211104163124p:plain

StoryPoint集計はAsana上で完結

Asanaにはカスタムフィールド機能があり、数値型を指定するとダッシュボード上で集計やグラフ化ができます。 これにより、チームのベロシティ計測が素早くできるようになりました。 f:id:techaskeninc:20211104163241p:plain ただし注意すべき点として、親タスクとサブタスクが同じプロジェクトに存在する場合は数値が重複して集計されてしまいます。そのため、サブタスクに数値を入力する場合には親タスクには数値を入力しないようにするなど、運用上の工夫が必要です。ほかにもサブタスクの数に上限があるなど、集計には注意すべきポイントがいくつかあります。実際に運用する前には、このあたりのAsanaの集計の振る舞いを確認しておくことをおすすめします。

ロードマップやスプリント中のタスクを時系列で可視化

Asanaには「タイムライン」という機能があり、任意のスケールでチケットを時系列で可視化できます。 チームでは、月スケールで四半期ごとのロードマップの可視化、日スケールでスプリント中のタスクを可視化し、それぞれが把握しやすくなるように活用しています。 f:id:techaskeninc:20211104163556p:plain

その他

AsanaはWeb GUIに力を入れているようで、ドラッグアンドドロップやチケット複数選択などで直感的にサクサクとチケット編集が可能です。とても便利です。

今後の取り組み

チームメンバーが「この作業はプロダクトゴール達成においてどういう位置づけなんだっけ?」と思ったときに、すぐ把握できる状態が望ましいと考えています。その観点で、Epicの切り方やそれらのロードマップ上の可視化の仕方にはまだまだ工夫の余地がありそうです。このあたりも、チームで考えながら少しずつ改善していければと思います。

まとめ

チームが一丸となって取り組むためには、情報の偏りを減らし、同じ文脈を共有したうえでコミュニケーションをとっていくことが重要です。見える化はその手段のひとつです。弊社では今後も不確実性を減らすための取り組みにチャレンジしていきます。

積極採用中です

askenでは現在エンジニアを絶賛募集中です。 興味のある方は是非こちらをご覧ください!!
www.wantedly.com

募集職種 — 株式会社asken (あすけん)www.asken.inc


  1. Asanaに移行を決めた時点ではGitHub Projectでは解決できない課題でしたが、2021年現在機能追加が進んでいるため、今後Github Project上で実現できるものも出てきそうです。

あすけんSlackのコメント分析〜MeCab・Sentencepiece・Word2Vecを添えて〜

はじめに

こんにちは!

askenでMLエンジニアとして働いているyumaです。shoku-pan🍞という名前でTwitterをやってます。

前回は社内Slackの人気絵文字ランキングを調べました。

tech.asken.inc

Slack Appを使ってSlackから絵文字データを抽出して、bar chart raceを使ってランキングをアニメーションで表示することができました。

今回もSlackデータの分析を行います。 askenのSlackコメントを分析して、どういったワードがよく使われているかを調べてみました\\\ ٩( 'ω' )و ////

全体の流れ

まず、全体の流れは以下になります。

  • Slack Appのインストール
  • 実装
    • Slackからコメント一覧を取得
    • 前処理
    • 形態素解析
    • WordCloud
    • N-gram
    • Sentencepiece
    • 単語の分散表現
    • 単語の分散表現の可視化

Slack Appのインストール

前回同様、Slackからデータを取得するためにSlack Appを作成・インストールします。Slack Appの作成方法は、こちらの記事を参考にしました。

qiita.com

作成後、Slackのワークスペースにインストールできればいよいよ実装です。

実装

今回も言語はPython、環境はGoogle Colaboratoryを使用しました。

Slackからコメント一覧を取得

それではまず、Slackからコメント一覧を取得するために必要な設定をしておきます。

TOKEN = '[token ID]'
channels = [
            '[channel ID1]',
            '[channel ID2]',
            '[channel ID3]',
            # 対象とするチャンネルを全て指定
]

コメントの取得にあたっては、[token ID][channel ID]が必要になります。

[token ID]には、Slack APIのUser OAuth Tokenを指定します。
[channel ID]の確認方法は以下を参照ください。

qiita.com

今回、以下5つのあすけんSlackチャンネルからデータを収集しました。

チャンネル名 説明
random 雑談用チャンネル
engineer エンジニア専用チャンネル
learning 日々学んだこと、情報やナレッジを共有するチャンネル
cat 🐈猫民の猫民による猫民のためのチャネル🐈
muscle 筋肉専用チャンネル

それでは、Slackからコメント一覧を取得してみましょう。

import pandas as pd
import requests
import re

def get_conversations_history(channel, limit):
    url = "https://slack.com/api/conversations.history"
    headers = {"Authorization": "Bearer "+TOKEN}
    params = {
        "channel": channel,
        "limit": limit,
    }
    return requests.get(url, headers=headers, params=params)    

text_list = []
for channel in channels:
    conversations_history = get_conversations_history(channel, limit=1000)
    for i in conversations_history.json()['messages']:
        if 'bot_id' not in i: # botは除く
            text_list.append(i['text'])

Slackでコメント一覧を取得するためのAPIconversations.historyです。

get_conversations_history関数で、指定したチャンネルのコメントを取得しています。 なるべく多くのデータを集めたかったので、limit=1000としました。 text_listに取得したデータが入っています。

前処理

次に、前処理としてテキストの整形を行います。

上記で取得した生データを見ると、以下のような文字列が含まれているのがわかります。

  • 12:34:時刻
  • :hoge::絵文字リアクション
  • <http://hogehoge>:URL
  • <@hoge>: メンション
  • $gt;:引用を表す文字列
  • 空白文字(スペースや改行)

これらは分析の対象外としたいので削除しておきます。

def preprocess_text(text):
    """テキストから不要な文字を削除"""
    text = re.sub(r'\d{1,2}:\d{1,2}', '', text) # 時刻(12:34など)の情報は削除
    text = re.sub(r':.+:', '', text) # リアクション(:smile:など)の削除
    text = re.sub(r'<http.+>', '', text) # urlの削除
    text = re.sub(r'<@.+>', '', text) # メンションの削除
    text = text.replace('&gt; ', '') # 引用部分の削除
    text = re.sub(r'\s', '', text) # 空白文字(スペースや改行)の削除    
    return text

preprocessed_text_list = [preprocess_text(i) for i in text_list]
preprocessed_text_list = [i for i in preprocessed_text_list if i != ''] # 空の要素を削除

preprocessed_text_listには、前処理済みのデータが入っています。

一旦テキストファイルに保存しておきましょう。

with open('slack_all_text.txt', mode='w') as f:
    f.write('\n'.join(preprocessed_text_list))

形態素解析

さて、分析するデータが準備できたので、次は形態素解析を行います。 今回は形態素解析器としてMeCab、辞書は新語や固有表現に強いmecab-ipadic-NEologdを使用します。

まずはMeCabや辞書をインストールします。
(インストールにはちょっと時間がかかります)

!apt-get -q -y install sudo file mecab libmecab-dev mecab-ipadic-utf8 git curl python-mecab > /dev/null
!git clone --depth 1 https://github.com/neologd/mecab-ipadic-neologd.git > /dev/null 
!echo yes | mecab-ipadic-neologd/bin/install-mecab-ipadic-neologd -n > /dev/null 2>&1
!pip install mecab-python3 > /dev/null
!ln -s /etc/mecabrc /usr/local/etc/mecabrc
!echo `mecab-config --dicdir`"/mecab-ipadic-neologd"

それでは形態素解析を行います。
形態素解析の結果、各単語に分かち書きされて品詞が付与されます。
今回は、重要な情報の多くは名詞が担っているはずだと考え、名詞のみを抽出対象にしました。

import MeCab
path = "-d /usr/lib/x86_64-linux-gnu/mecab/dic/mecab-ipadic-neologd"
tagger = MeCab.Tagger(path)

def morphological_analysis(text):
    """形態素解析して結果をリストを返却"""
    node = tagger.parseToNode(text)
    result_list = []
    # pos_list = ['名詞', '動詞', '形容詞'] # 対象とする品詞を指定
    pos_list = ['名詞'] # 今回は名詞のみ対象
    while node:
        surface = node.surface
        feature = node.feature
        pos = feature.split(',')[0]
        if surface and pos in pos_list:
            result_list.append(surface)
        node = node.next
    return result_list

word_list = [] # すべての単語リスト(1次元リスト)
word_list_by_sentence = [] # 文ごとの単語リストのリスト(2次元リスト)
for text in preprocessed_text_list:
    result_list = morphological_analysis(text)
    word_list += result_list
    word_list_by_sentence.append(result_list)

word_listは名詞のリスト(1次元リスト)、word_list_by_sentenceはセンテンス(文)ごとの名詞のリストのリスト(2次元リスト)となっています。

WordCloud

ここまでで、Slackのコメントで使われている大量の名詞データが手に入りました。ここからがいよいよ分析です!

まず、どういったワードがよく使われているかを調べるため、WordCloudを使ってみます。WordCloudとは、文章中で出現頻度が高い単語ほど大きく表示する図法のことです。

WordCloudで日本語を表示するために必要なフォントをあらかじめインストールしておきましょう。

!wget https://noto-website-2.storage.googleapis.com/pkgs/NotoSansCJKjp-hinted.zip
!unzip NotoSansCJKjp-hinted.zip

それでは、WordCloudを描いてみます。

import matplotlib.pyplot as plt 
from wordcloud import WordCloud

def plot_wordcloud(text, max_font_size=200, min_font_size=10, background_color='black'):
    """単語ごとにスペースで区切られたテキストを入力として、wordcloudを表示する"""
    wordcloud = WordCloud(
        font_path='NotoSansCJKjp-Black.otf',
        width=900, height=700,
        background_color=background_color,
        max_font_size=max_font_size,
        min_font_size=min_font_size,
        collocations = True,
        ).generate(text)
    plt.figure(figsize=(15,12))
    plt.axis("off")
    plt.imshow(wordcloud)
    plt.savefig("word_cloud.png")
    plt.show()

text = ' '.join(word_list)
plot_wordcloud(text, max_font_size=200, min_font_size=10, background_color='black')

f:id:techaskeninc:20211022130821p:plain

うまく表示することができました!
頻出のワードほど大きく表示されています。
ここで、パッと見でもわかるように、「こと」「そう」「ため」などといった情報を持たないワードが大きく出てしまっています。
こういった、一般的すぎるが頻出するワードのことを「ストップワード」といい、処理の対象から除外するのが一般的です。

ストップワードを削除した結果は以下になります。

f:id:techaskeninc:20211022130839p:plain

先程まで目立たなかった「お願い」「今日」「アプリ」といったワードが全面に出てきましたね!

「お願い」は「よろしくお願いします」という形で非常に多く使われています。 また、「今日」は「今日もよろしくお願いします。」「今日の〜」といったコメントが多いために大きく表示されているようです。 「アプリ」は、弊社askenのダイエットアプリあすけんやその他アプリについて言及されることが多く、頻出ワードとなっています。

www.asken.jp

さて、ストップワードの除去には、

  • 辞書による方式
  • 出現頻度による方式

の大きく2つがあり、今回は辞書による方式を採用しました。

一方、出現頻度による方式とは、単語の頻度をカウントして頻度の高いもの(場合によっては低いもの)を除外する方法です。高頻度の単語は、全体に占める割合が大きいにもかかわらず、重要な情報を持っていないことが多いという考えに基づいています。

さらに、単語の出現頻度 {\rm tf}(term frequency:単語の出現頻度)をそのまま用いるのではなく、それに単語が出現する文書数の逆数 {\rm idf}(inverse document frequency:逆文書頻度)をかけて考えるtf-idfという方法があります。

 
\displaystyle
{\rm tf \text{-} idf}(t, d) = {\rm tf}(t, d) \times {\rm idf}(t, d)\\

\displaystyle
{\rm idf}(t, d) = {\rm log}\frac{N}{1+{\rm df}(t)}

ここで、  {\rm tf}(t, d)は文書 dにおける単語 tの出現頻度、 {\rm df}(t)は単語 tが出現する文書数、 Nは全文書数です。

tf-idfでは、多くの文書に出現する語の重要度を下げ、逆に特定の文書にしか出現しない単語の重要度を上げることができます。
その結果、各文章に特徴的な単語を抽出することができ、ある程度ストップワードも取り除くことができると考えられます。

今回、tf-idfは使用しませんでしたが、気になる方はこちらのたかぱいさんの記事が大変わかりやすいのでご覧ください。

www.takapy.work

N-gram

さて、WordCloudを描いて頻出のワードを調べてみました。
これは、ワード単体1で見るのには大変便利で、見た目のインパクトもあって大変面白い方法です。
では次に、どういった組み合わせで単語がよく使われているかを調べるため、N-gramを使ってみましょう。 N-gramを用いると、連続して使われる単語(あるいは文字)を調べることができます。

from collections import defaultdict
import plotly.graph_objects as go

def n_gram(word_list_1d, n):
    """ngramのリストを返す"""
    ngram_list = []
    for i in range(len(word_list_1d)-n+1):
        ngram_list.append(' '.join(word_list_1d[i: i+n]))
    return ngram_list

def show_bar_plot(df, color):
    """棒グラフを表示する"""
    fig = go.Figure(go.Bar(
                x=df["wordcount"].values[::-1], #少ない順に並んでしまうので逆順にする
                y=df["word"].values[::-1], # xと同様
                showlegend=False,
                orientation = 'h',
                marker=dict(
                    color=color,
                ),
    ))
    fig.update_layout(
        margin=dict(l=100, r=20, t=20, b=20),
        paper_bgcolor="LightSteelBlue",
        height = 200,
        width = 1000
    )
    fig.show()

def show_ngram_bar_plot(word_list_2d, n, top_n=30, color='blue'):
    """ngramの結果を棒グラフで表示する"""
    freq_dict = defaultdict(int)
    for word_list_1d in word_list_2d:
        for ngram in n_gram(word_list_1d, n):
            freq_dict[ngram] += 1
    df = pd.DataFrame.from_dict(freq_dict, orient='index')
    df.sort_values(0, ascending=False, inplace=True)
    df = df.reset_index().set_axis(['word', 'wordcount'], axis='columns')
    show_bar_plot(df[:top_n], color=color)

# unigram
show_ngram_bar_plot(word_list_by_sentence, n=1, top_n=10, color='blue')
# bigram
show_ngram_bar_plot(word_list_by_sentence, n=2, top_n=10, color='green')
# trigram
show_ngram_bar_plot(word_list_by_sentence, n=3, top_n=10, color='red')

f:id:techaskeninc:20211022130806p:plain

unigram(N=1)、bigram(N=2)、trigram(N=3)のトップ10を棒グラフで出してみました。

unigramはWordCloud同様、単体で使われるワードを多い順に並べたものになっています。

bigramには、「業務/開始」「コンテキスト/マップ」「在宅/変更」といった単語の組み合わせがランクインしています。これは、勤怠の連絡やDDD(ドメイン駆動設計)の勉強の話が多くされていることを表しています。askenではDDDをはじめ、多岐にわたるテーマの勉強会が活発に行われており、Slackでもよく意見が交わされているようですね。

ところで「みゅう/ちゃん」とは、社員が飼っている猫の名前です。askenには猫好きな社員が多く(犬好きもいます)、オンライン会議ではお目見えすることもあります

trigramには「筋トレ」というワードが入っています。仕事柄健康に対する意識の高さから、食だけではなく運動も強く意識する社員が多いですね。最近ではランニングや登山といった話題が人気のようです。

Sentencepiece

ここまで、WordCloudおよびN-gramを使って、頻出ワードを調べてみました。

ところで、これらはいずれも辞書を用いた分析であり、実は辞書に登録されていない単語(未知語)にはうまく対応できません。たとえば、bigramの結果をあらためてよく見ると「あす/けんが」とあります。これは、使用した辞書(NEologd)に「あすけん」という単語が登録されていない未知語のためです(早く登録してもらえるように頑張ります…)

そこで、辞書に頼るのではなく、データを学習してより未知語にも対応できるようにすることを考えます。そこで登場するのがSentencepieceという手法です。

Sentencepieceとは、ざっくりと言うと、従来の「文法的に正しい分割」ではなく、学習データである生のテキストから最適な分割点を学習しようというものです。

詳細は、しんちろさんのこちらの記事がすごくわかりやすいので参考になさってください。

buildersbox.corp-sansan.com

Sentencepieceを使うために、インストールしておきましょう。

!pip install sentencepiece

また、Sentencepieceと辞書ベースのMeCab分かち書きを比較するための関数も用意しておきます。

import sentencepiece as spm

def tokenize_mecab(text):
    node = tagger.parseToNode(text)
    result_list = []
    while node:
        surface = node.surface
        if surface:
            result_list.append(surface)
        node = node.next
    return result_list

# しんちろさんのコードを参考にしました
def tokenize_sp(input_text: str, model_path: str) -> list:
    '''SentencePieceによる分かち書き'''
    # モデルの読み込み
    sp = spm.SentencePieceProcessor()
    sp.Load(model_path)
    # sentencepieceによる分かち書き
    tokenize_list = sp.EncodeAsPieces(input_text)
    # 必ず最初に'▁'が入るため削除
    tokenize_list = [token.replace(
        '▁', '') for token in tokenize_list if token.replace('▁', '') != ""]
    return tokenize_list

それではSentencepieceを使って学習を行いましょう。
保存しておいた前処理済みのテキストデータのファイルを読み込みます。

spm.SentencePieceTrainer.Train(
    input='slack_all_text.txt',
    model_prefix='sentencepiece',
    vocab_size=2000,
    character_coverage=0.9995
)

学習が終わったら分かち書きしてみましょう。
MeCabの結果と比較すると以下のようになりました。

text = 'あすけんはダイエットアプリです。'
print(tokenize_mecab(text)) # Mecab
print(tokenize_sp(input_text=text, model_path="sentencepiece.model")) # sentence piece
# ['あ', 'すけん', 'は', 'ダイエットアプリ', 'です', '。']
# ['あすけん', 'は', 'ダ', 'イ', 'エ', 'ット', 'ア', 'プ', 'リ', 'です', '。']

MeCabでは「あ/すけん」と分かれてしまっていますが、Sentencepieceでは「あすけん」と正しくわけられていますね! 「あすけん」というワードが多くの文章に登場しており、分割点をうまく見つけられたようです。

一方で、「ダイエットアプリ」というワードに関しては、細かく分かれすぎていてMeCabが勝った形となりました。 Sentencepieceの学習に関しては、「あすけん」というワードは十分な数あるが、「ダイエットアプリ」というワードは十分ではなかった、と考えられます。

単語の分散表現

ここまでで、MeCabを使った辞書ベースの分析、Sentencepieceによる学習と分析をやってきました。

次に、単語の分散表現について見てみようと思います。 分散表現(あるいは単語埋め込み、word embedding)とは、単語を高次元の実数ベクトルで表現する技術です。分散表現を得るために、Word2Vecを使用します。

Word2Vecを使用するために、gensimをインストールしておきます。

!pip install gensim

インストールできたら、Word2Vecのモデルを作成してみましょう。
センテンスごとに分けた単語のリストword_list_by_sentenceに対して学習を行います。

from gensim.models import word2vec

model = word2vec.Word2Vec(word_list_by_sentence, size=300, min_count=5, window=5, iter=100)
model.wv.save_word2vec_format('word2vec.bin', binary=True) # モデルの保存

学習が終わったら、単語の分散表現を確認してみましょう。
例として、「ダイエット」という単語を見てみます。

print(model.__dict__['wv']['ダイエット'])
# [ 0.52999353 -0.23631863 -0.00984514 -1.1539422   0.65169364 -0.33282238
#  -0.8465534  -0.06162367  2.0389488  -0.03625611  0.54652476 -0.83955187
#  -0.6747863  -0.5368701  -0.2588077   1.1441534  -0.25403473  0.22454463
#   0.10980017  0.90397495  1.97433    -1.2201335  -0.85706323 -0.06221941
#   1.7707006   0.39611042 -0.66982204  0.0743041  -0.33158022 -1.0384009
#  (以下略)

上記では省略していますが、300次元のベクトルになっていることが確認できました。

さて、ベクトル間では類似度を計算することができます。

たとえば、2つのベクトル \vec{x}=(x_1, x_2, ..., x_n), \vec{y}=(y_1, y_2, ..., y_n)のコサイン類似度は以下になります。

 
\displaystyle
{\rm similarity}(\vec{x}, \vec{y}) = \frac{\vec{x}\cdot\vec{y}}{\|\vec{x}\|\|\vec{y}\|} = \frac{\sum_{i=1}^{n}x_iy_i}{\sqrt{\sum_{i=1}^n}x_i^2 \sqrt{\sum_{i=1}^n}y_i^2}

コサイン類似度は、1に近いほど類似度が高く、-1に近いほど類似度が低いことを表します。

さて、例として「沼」という単語に近い単語トップ10を出してみました。

model.wv.most_similar(positive=['沼'])
# [('マグマ', 0.774527370929718),
#  ('マッスルグリル', 0.732177734375),
#  ('ツイート', 0.7176527380943298),
#  ('やってみよう', 0.6925275921821594),
#  ('ww', 0.6841464042663574),
#  ('シャイニー', 0.6801939010620117),
#  ('オートミール', 0.6785858869552612),
#  ('素人', 0.667254626750946),
#  ('セメント', 0.6630067825317383),
#  ('減量', 0.6545518636703491)]

急に「沼」というワードを出してしまいましたが、これはれっきとした料理の名前です。
マッスルグリルのシャイニー薊さんが考案した究極の減量食で、炊飯器さえあれば簡単につくることができ、界隈では大変話題になっています。
「沼」についてはこちらの動画で紹介されています。

youtu.be

結果を見てみると、「マッスルグリル」や「シャイニー」、「減量」といった単語がランクインしており、うまくベクトル化できているようです。 ちなみに、「マグマ」や「セメント」もシャイニーさん考案の料理です。

なお、あすけんでは「沼」「マグマ」「セメント」もメニュー登録されていますw
ぜひアプリで検索してみてくださいね

単語の分散表現の可視化

それでは最後に、上記の分散表現を可視化してみましょう。
といっても、300次元の単語ベクトルをそのまま扱うことはできないので、今回はPCA(主成分分析)で次元削減し、3次元にプロットしてみます。

可視化にあたっては、TensorBoardを使用するのでインストールしておきます。

!pip install torch tensorboardX tensorflow

さらに、さきほど作成したモデルを読み込んで、単語ベクトルの情報をファイルに出力します。

import gensim
import torch
from tensorboardX import SummaryWriter

writer = SummaryWriter()
model = gensim.models.KeyedVectors.load_word2vec_format("word2vec.bin", binary=True)
weights = model.vectors
labels = model.index2word
writer.add_embedding(torch.FloatTensor(weights), metadata=labels)

metadata(各単語)とvector(各単語の分散表現)の2つのtsvファイルが作成されますので、これらをTensorBoardを使って描画してみます。
こちらにファイルをアップロードすることでも表示できます。

%load_ext tensorboard
%tensorboard --logdir runs

単語ベクトルが3次元空間にうまくプロットできていますね。

「出社」「DDD」「タンパク質」「沼」「ネコ」という単語に類似する単語トップ10もあわせて表示してみました。
それぞれ、3次元空間上でもある程度まとまっていることが確認できます。

たとえば「タンパク質」を見てみると、「プロテイン」や「ホエイ」「ソイ」そして「牛乳」といったワードが周辺に分布しています。
これらの単語は組み合わせて使われることが多く、そのため特徴量をうまく抽出できているのではないかと考えました!

まとめ

いかがでしたか?

今回は、弊社askenのSlackコメントを分析してみました。

今回の内容をまとめると、以下のようになります。

  • Slack APIを使って、Slack内のデータを収集することができる
  • MeCabを使うことで辞書ベースの分析ができる
  • WordCloudを使うことで、頻出のワードを大きく表示してインパクトのある図を描くことができる
  • N-gramを使うことで、どういった組み合わせで単語が使われているかを知ることができる
  • 辞書に登録されていない未知語をデータからうまく抽出するために、Sentencepieceという手法がある
  • Word2Vecを使用することで、単語の分散表現が得られる
  • 分散表現は、類似度を計算したり、次元削減したものを3次元(あるいは2次元)にプロットしたりできる

ぜひ、皆さんも会社のSlackコメントを分析してみてくださいね

お知らせ

askenでは、一緒に働いてくれるエンジニアを募集しています!
www.wantedly.com

主な参考資料


  1. 正確には、WordCloudのオプションでcollocations = Trueとしているので、bigramまで表示できています。Falseとすれば単語単体で表示することができます。

askenエンジニア組織紹介

はじめまして!
askenで人事採用を行っている、平賀(ひらが)です。 2020年にasken初の人事採用担当者として入社、エンジニアをはじめ様々な職種の採用や人事、採用広報を行っています。

はじめに
この度、有志でエンジニアにも参加してもらい、念願だったテックブログを開始させていただきました!エンジニア発信で、askenの技術的な取り組みや日々の工夫などを発信していきますが、技術的なテーマだけではなく、組織の雰囲気やaskenエンジニアの生態、面白小ネタなども少しずつご紹介したいと思います。

今回、非エンジニアではありますが、エンジニア採用担当の私平賀が筆をとらせていただきました。 少しでも組織の雰囲気やどんなエンジニアが活躍しているか知っていただける機会になればうれしいです。

組織構成

まず、askenの組織構成について簡単にご紹介します。図のように、大きく5つの部門で構成されています。

f:id:techaskeninc:20210930153024p:plain

  • コンシューマー事業部…食事管理アプリ「あすけん」の企画、マーケティング、広告事業
  • 法人事業部…「あすけん」の法人利用事業、医療事業
  • 海外事業部…アメリカ・カナダで展開している「Asken diet」の企画、マーケティング
  • システム部…askenの全サービスのシステム企画開発
  • 管理部…人事、総務、経理等の本社機能等

2021/9 現在、13名のエンジニアは全員、システム部に所属しています。
プロダクトごとに事業部が分かれ、システム部は事業部横断で各メンバーが各プロダクトの開発を担当しています。

現在のシステム部の構成はこのようになっています。

f:id:techaskeninc:20210930153040p:plain

アプリチーム5名、サーバーサイドチーム5名、AIチーム2名、VPoEと部長の計13名。
その他、外部のパートナーの方々10数名と共にサービス開発を進めています。
インフラチーム(※)は立ち上げフェーズとなっていて、現在サーバーサイドチームのMGRが兼務しています。
平均年齢は35.07歳、25歳~43歳の男性で構成され、askenの創業期から関わり勤続10年弱の人から、入社半年ほどのニューフェイスもいて和気あいあいと仕事をしています(眼鏡率...69.2%) 。

リモートワークを導入し、対面でのMTGやコミュニケーションの機会はエンジニアだけではなく、全社的に激減していますが、毎日の朝礼や夕礼、雑談Slackチャネルなど、様々なコミュニケーションツールを使って話をしています。

オンライン上で久しぶりにカメラONで会うと、ワイルドな髭スタイルになっていたり、会社で会うたびに髪色が変わっていたり、Zoom背景が変わる頻度が高い人がいたり、なぜか部屋が暗くて「部屋暗っ!!本人も見えない!」とつい突っ込みたくなる人がいたり、自分らしくお互いに尊敬しあいながら仕事をしているのがうかがえる雰囲気です。

出社頻度

さて、リモートワークなどの働き方についてお話しましたが、具体的にどのくらいの頻度で出社してるのかアンケートを実施しました。

f:id:techaskeninc:20210929203326p:plain

2~3か月に1日程度またはそれ以下の人は6名(46.2%)、約半分のエンジニアはほとんど在宅で勤務しています。8~9月は社内の健康診断受診期間もあったので、最近久しぶりに出社したという人も多いですね。

1か月に1日程度は2名(15.4%)、1か月に2~3日程度は1名(7.7%)、1週間に1~2日出社は2名(15.4%)、1週間に3日~毎日の人は1名(7.7%)という結果になりました。

「エンジニアはほとんど出社しない」という企業も多いと思いますが、askenでは在宅が飽きてくると出社する人や、会社の近くに引っ越す人(=出社率高い)など、オフィスでの業務もしやすいと感じている人が多いようです。

エンジニアたちの勉強方法

常に新しい技術が生み出され、勉強をし続けることが当たり前になっていますが、askenエンジニアが普段どんな方法で勉強しているか聞いてみました。

f:id:techaskeninc:20210929203408p:plain

ほとんどのエンジニアが、日頃から書籍で勉強をしていました。
リアルでの勉強会に積極的に参加していた人はオンラインで参加したり、他社のエンジニアとの交流など、様々な手法で自己研鑽を行っています。

asken社内でも、アプリチームやサーバーサイドチームなど、チームごとで勉強会の実施や成功/失敗事例を共有したり、自身の開発業務の中で役立つツールや技術を学習用チャネルや全社情報共有ツールesaで共有しています。

次に、今まさに勉強していること、勉強したいと思っていることについても聞いてみました。

f:id:techaskeninc:20210930154008p:plain

ドメイン駆動設計、スクラム開発、SREなどなど、askenのプロダクト開発に直結する技術関連の項目が一番多い結果になりました。一方で、技術だけではなく、組織論、経営、マーケティングコーチングなど、様々な分野へ興味をもっていることもわかりました。

自身の技術力向上だけではなく、組織として、会社として成長し続け、社会に貢献していくというビジョンと併せて、askenで働くメンバーそれぞれが自分らしく、前向きに働ける組織づくりへの興味関心も感じられる結果となりました。

休日の過ごし方

日々、業務中は真剣に開発に取り組んでいるエンジニア衆。
休日はどのように過ごしているか聞いてみました!

f:id:techaskeninc:20210929203426p:plain

休日は運動をしたり、ゲームや動画を観たりとゆっくりと過ごす人が多いですね!
askenは全社的に既婚者が多いのですが、エンジニア組織も13名中9名が既婚者ということで(眼鏡率と一緒…)家族と一緒に過ごすと回答した方も多かったです。

コロナ前は、メンバーで登山に行ったりとアクティブな活動もしていたそうなので、今後そういったイベントも行えるようになるといいですね!

健康であるために心がけていること

askenは「ひとびとの明日を今日より健康にする」というビジョンを掲げ、事業を行っています。 そのため、社員も健やかに日々の生活を送れるよう意識しています。
そこで、日頃健康であるために、どのようなことを心がけているか聞いてみました!

f:id:techaskeninc:20210929203437p:plain

「食事」「運動」など、あすけんアプリで食事管理もしながら健康管理してるんですね!
「寝る」ことも大事です!

システム部のいいところ

全員が中途入社、それぞれに得意分野を持った13名のエンジニア達ですが、お互いに組織やメンバーのことをどう思っているのか、システム部のいいところを聞いてみました!

f:id:techaskeninc:20210929203452p:plain

今回は魅力、いいところだけを聞きましたがw
「いい人が多い」「優しい!」「勉強熱心」などの声が多い結果になりました。
「声をかけやすい」というのも大きな特徴のようです。業務中、困ったことやトラブルがあったとき、一人で抱えずにすぐにアラートを上げて皆で対処するという光景も当たり前にできている印象です。

チームで仕事をすることも多いですが、仲良くお仕事できてる様子。これから組織を拡大し、新しい仲間が増えていってもこのaskenシステム部の風土を大切にしていきたいですね !

まとめ

いかがでしたか?

ほんの一部ですが、askenのエンジニア組織のご紹介をさせていただきました。
少しでも私たち組織の雰囲気やどんな人たちが働いているのかが伝われば幸いです^^

これからも、いろいろな切り口で組織や取り組みなどをご紹介していく予定です!!
よろしくお願いします!

f:id:techaskeninc:20210929214647j:plain ※直近(と言っても数か月以上前)、一番エンジニア衆が集まった時の1枚


askenでは、一緒にサービスをつくってくれるエンジニアを大募集しています。
興味のある方は是非こちらご覧ください!!
wantedly
採用HP

iOSDC 2021 「DateComponentsと仲良くなる」

初めに

askenの @sato-shin です。 iOSDC 2021 で「DateComponentsと仲良くなる」という題目で登壇させていただきましたので、記事としても残しておきます。

スライド

speakerdeck.com

YouTube


www.youtube.com

この発表をしようとしたきっかけ

弊社askenでは「あすけん」というサービスを開発しています。食事や運動などの記録を行い、生活習慣改善をサポートするサービスです。 あすけんでは朝食、昼食、夕食、間食の区分に分けて食事記録を行えるのですが、このとき1日という表現がとても大事になってきます。 この1日の表現にDate型を使っていたのですが、タイムゾーンの変更に弱く、取り扱いが難しい課題がありました。 これを解決するのにDateComponentsを利用しました。 この改善を通して、「DateComponentsは便利。もっと話題になっても良いのでは?」と思い、CfPを出すことを決めました。

DateComponentsと仲良くなる

時間の基礎

私たちはよく時間を "2021年9月19日 13時30分" というように年月日時分(+秒)を使って時間を認識することが多いと思います。 しかし、この表現方法のルールはとても複雑でコンピュータ上では扱いが簡単ではありません。 コンピュータにとって最も簡単な時間を定義する方法は、「基準点を決め、そこからの長さ」によって、時間を定義することです。

f:id:techaskeninc:20210917182709p:plain

Date型

この時間の定義をそのまま実装したクラスとして、Swift では Date型 が使われます。Date型のイニシャライザを見ると、それがよく分かります。

  • init(timeIntervalSinceNow: TimeInterval)
  • init(timeIntervalSince1970: TimeInterval)
  • init(timeIntervalSinceReferenceDate: TimeInterval)
  • init(timeInterval: TimeInterval, since: Date)
  • init() = (timeIntervalSinceNow: 0) と同義

Sinceは基準点で、TimeIntervalは基準点からの長さを表し単位は秒です。 Sinceに使える特徴的なものとして、以下のものがあります。特にUnixEpochはUnixTimeにおける基準点で、とてもよく使われます。

なお、TimeInterval は Double の alias です。

英語でDateといった場合に想像するものと違うので注意してください。Date型の正体は時間軸上の一点です。

日時計算の難しさ

Date型の表現はコンピュータにとっては扱いやすいのですが、人間にとっては扱いづらいです。 やはり私たちが日付を認識するときには、2021年9月19日 9時30分といった表現を使います。 しかしこの表現のルールは思った以上に複雑です。

  • 1年
  • 1ヶ月
    • 閏年ではない2月 = 28日間
    • 閏年の2月 = 29日間
    • 4, 6, 9, 11月 = 30日間
    • 1, 3, 5, 7, 8, 10, 12月 = 31日間
  • 1日
  • 1分
    • 閏秒が挿入されない = 60秒間
    • 閏秒が挿入される = 61秒間

年や月だけでなく、日や分の長さも固定長ではないことが分かります。さらにサマータイムタイムゾーンに依存するので、タイムゾーン特有の処理も必要になります。 これらを自前で全て網羅しつつ日時計算を行うのはあまりにも非効率です。そこで、多くのプログラミング言語では日時計算を行うものが提供されています。 SwiftではCalendar型が日時計算を担当する型として定義されています。

Date型では表現できないもの

あなたの生年月日をDate型で表現するとしたらどうなるでしょうか? 実はDate型では正確に表現することはできません。 生年月日が持っている情報は、「タイムゾーンに依存しない特定の1日」と言えるでしょう。 このタイムゾーンに依存しないというのが厄介です。 たとえば、日本の1日とニューヨークの1日というのはタイムゾーンによって、「ずれ」があります。 Date型は時間の点なので、この「ずれ」を表現することはできず、生年月日を正確に表現できません。

DateComponents型

そこで、もっと私たちが日常的に使っている日付の形式で表現できる型があると便利です。 この日付の形式で表現できる方というのがDateComponents型です。 DateComponents型は日時の構成要素の集合です。 この構成要素の数が多いため、DateComponentsのイニシャライザの引数は16個と多くなっています。

init(calendar: Calendar?, timeZone: TimeZone?, era: Int?, year: Int?, month: Int?, day: Int?, hour: Int?, minute: Int?, second: Int?, nanosecond: Int?, weekday: Int?, weekdayOrdinal: Int?, quarter: Int?, weekOfMonth: Int?, weekOfYear: Int?, yearForWeekOfYear: Int?)

全てoptionalとなっているため一度に全て利用しなければいけないわけではありません。表現したい時間に必要なものだけを指定します。 また、要素一つ一つは簡単です。

  • calendar: Calendar? = カレンダー(暦)
  • timeZone: TimeZone? = タイムゾーン
  • era: Int? = 時代、年号
    • グレゴリオ暦では 0 が紀元前を、1 が西暦を表す
    • 和暦では、0 が大化を、249 が平成を、250 が令和を表す
  • year: Int? = 年
  • month: Int? = 月
  • day: Int? = 日
  • hour: Int? = 時
  • minute: Int? = 分
  • second: Int? = 秒
  • nanosecond: Int? = ナノ秒
  • weekday: Int? = 曜日
  • weekdayOrdinal: Int? = その曜日が何番目か
    • たとえば、2021年9月19日は第日曜日
  • quarter: Int? = 四半期
  • weekOfMonth: Int? = その月の何週目か
    • たとえば、2021年9月19日は第4週目
  • weekOfYear: Int? = その年の何週目か
  • yearForWeekOfYear: Int? = weekOfYear計算用の年

DateComponents型の3つの表現

DateComoponents型は日時の構成要素の集合であり、実際にどのような意味を持つかはプログラムによって異なります。 意味のパターンとしては、大きく分けて3つあります。

  • 日時を表す
  • 量を表す
  • パターンマッチングを表す

日時を表す

この表現は、私たちがよく利用する "2021年9月19日 13時30分" などといった日時を表現することを目的とし、Date型へと変換可能な表現になります。

let today = DateComponents(calendar: gregorianCalendar, year: 2021, month: 9, day: 19, hour: 13, minute: 30)

DateComponents型では、意識しないものについては指定しなくて良いので、「Date型では表現できないもの」のところで挙げた生年月日を正しく表現することができます。 たとえば、私の誕生日は西暦1990年10月18日なのですが、これをDateComponents型で表現すると以下のようになります。

let birthday = DateComponents(calendar: gregorianCalendar, year: 1990, month: 10, day: 18)

ここで着目して欲しいのが、TimeZoneを指定していないということです。 生年月日が表したい「タイムゾーンに依存しない特定の1日」というものを正確に表現できています。

量を表す

この表現では時間の長さを表します。時間の量を表す型としてTimeInterval型がありますが、これは単位が秒しか扱えませんでした。 DateComponents型を使うことで、年、月、日、時、分といった長さも正しく表現できるようになります。 この量の表現と、Calendar型の日時計算関数を利用して日時計算を行うことができます。 たとえば、今から一ヶ月後を計算する場合には以下のように書けます。

let oneMonth = DateComponents(month: 1)
let oneMonthLater = calendar.date(byAdding: oneMonth, to: Date())

パターンマッチを表す

毎朝、決まった時間に目覚まし時計を設定している人は多いと思います。 この表現はそんなときに利用できます。 たとえば、朝の8時30分に通知したい場合のUserNotificationTriggerを作りたい場合には以下のように書けます。

let wakeUpAlertTime = DateComponents(hour: 8, minute: 30)
let trigger = UNCalendarNotificationTrigger(dateMatching: wakeUpAlertTime, repeats: true)

Tips

DateComponents型がここまで説明したどの意味を持つかはプログラムによって異なります。 たとえば、コードを以下のような切れ端で見たときに、

DateComponents(hour: 12)
  • 12時間という「量」の表現
  • 12時という「パターンマッチ」の表現

このどちらの表現なのかは察することはできません。プログラムの前後の文脈から判断するしかありません。 ですので、作っているアプリケーションでどのような表現が欲しいのか?を分析し、それに合うようなラッパークラスを作るのをお勧めします。 たとえば、時間の長さが必要であるといった場合には以下のようなラッパークラスを作るだけでも、どんな表現をしたいのか?が分かりやすくなります。

struct DateLength {
    var value: DateComponents
}

最後に

Date型は原始的な時間の表現であり、単純なケースではとても扱いやすいです。 しかし、Date型では表現しきれなかったり、複雑になるケースも多くあります。 DateComponents型がその問題を解決してくれる可能性があります。 実際に私は開発内で、DateComponents型を使うことでコードをスッキリさせることができました。

「これまでDate型しか使ったことなかったよ」という人は、これを期にDateComponents型を使った方が良いケースがないかどうか考えてみてください。

We are hiring!

askenでは、iOSエンジニアを募集しています! www.wantedly.com