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

なぜエンジニアはマネージャーになるのに不安を覚えるのか

こんにちは。askenでエンジニアリング戦略や組織づくりを担当しているやすにしです。

マネジメントを中心にしておりまして、せっかくなのでブログでもマネジメントについて書いてみますね。

私はこれまでVPoEとしてエンジニア組織のマネジメントや、様々な会社でマネージャー向けにコーチン1 をやってきました。そこで接してきたエンジニアリングマネージャーに共通しているのは「キャリアに悩んでいる」ということです。
例えばこのようなことです。

  • コーディングをしなくなり、技術的に取り残されて、エンジニアとしてやっていけなくなる感じがする
  • 自分でやらないから成果が見えない。やっている感じがしない。
  • マネジメントをどうやればいいか、どう学べばいいかわからない。
  • マネージャーのキャリアで自分は定年(?)まで生きていけるのか?

共通点は、色々理由を言葉にしているものの、どれもしっくりきている感じではなく、「なんとなく不安」という感じでした。私自身は、組織に課題感を感じることが多い人種だったので、以前よりマネジメントにも興味を持ち色々やってきたものの、同じような不安を感じることがありました。だからこそ、今はプロダクトづくりにも時間を割いていることは確かです。

そこで、実体験や他のエンジニアリングマネージャーのお話から、私なりになぜ不安なのかを整理してみました。

エンジニア(プレイヤー)→マネージャーは何が変わるのか?

もともとエンジニア(あるいはプレイヤー)だった方がマネージャーになることがほとんどだと思いますが、エンジニア→マネージャーになると何が変わるのでしょうか。

一般的には「リーダーシップ」と「マネジメント」が求められると言われています。 ふむふむ、そうだよねと思いつつ、リーダーシップとはなんでだと思いますか?マネジメントはなんだと思いますか?

答えは無限にあり、唯一の正解はない世界ではありますが、私なりに定義をしてみました。

f:id:techaskeninc:20210908113504p:plain

リーダーシップ:一歩踏み出す

リーダーシップは、一部の人に求められることではなく、すべての人に求められることです。戦略を作り組織の先頭に立つような大きなこともそうですが、シーンとしている会議で最初に発言する、会議でホワイトボードの前に自ら立つなども大事なリーダーシップです。それぞれに共通的な行動とは何かを言語化してみた結果、「一歩踏み出す」を選んでみました。

マネジメント:チームを成果に導く

マネジメントは、自分で手を動かして成果を出すのではなく、チームが成果を出せる状態にすることだ、という意味でこの言葉を定義してみました。ドラッカーエッセンシャル版マネジメント

組織をして高度の成果をあげさせることが、自由と尊厳を守る唯一の方策である。
その組織に成果をあげさせるものがマネジメントであり、マネージャーの力である。

と言っています。

答えがわからない上に、やったことがないことを求められる

マネージャーになり、リーダーシップとマネジメントを求められた時に、この事実を目の当たりにします。

f:id:techaskeninc:20210908113815p:plain

つまり、答えがないことに答えを出し、自分がやらずに成果を出すことをやらなければなりません。そして、エンジニア(プレイヤー)としてやってきた 仕事のやり方を大きく変える 必要があり、このように戸惑います。

f:id:techaskeninc:20210908113958p:plain

答えがわからない」上に、プレイヤー経験のみの方は「やったことがない」ことをやらざるを得なくなるのです。これは戸惑うのが当たり前です。みんなそうです。私も未だに変わりません。

これが、プレイヤーからマネージャーになったときに戸惑ってしまう理由だと私は考えています。

答えがない事実を受け入れ、学び続けること

じゃあどうすればいいか。私が学んできたリーダーシップやコーチング経験から考察してみます。

まず大事なのは「答えがない事実を受け入れる」ことです。すべてのことに答えはありません。人によって違う認識があり、違う選択があるだけです。つまり、自分で答えを出すしかありません。大きい組織や影響力が大きい意思決定においてはなおさらです。いくら情報を集めても判断の決め手はなく、決して断つ、つまり決断が必要な場面ばかりです。リーダーシップを発揮するというのはそういうものです。それを受け入れ、自分で考え、決めていくしかありません。個人の人生における選択も答えがない問いの連続なので、誰しも必要なことかもしれません。

答えがないことに向き合う上で、もう一点大事なのは、「学び続けること」です。わからない、できない自分を受け入れ、どんなこと、どんな人からも学ぶ。そして、仮説と検証とふりかえり。つまりアジャイルな人になるということです。

大人の学びとは「悦び」であり、「痛み」でもある と言われています。わからないことに対峙すると、ときに居心地の悪いこともあります。でもそこに向き合い乗り越えていくことで、「わかった!」「できるようになった!」という悦びを得ることができます。 もしこのような学びに対峙できれば、視野が広がり、人間的な成長をすることができるなあと個人的に実感しています。マネージャーをやった結果の学びなのに、エンジニアとしての視野も広がり、技術に対する景色も変わってきます。

エンジニアとマネージャーの共通点

では、エンジニアとマネージャーはぜんぜん違うから、エンジニアのスキルは活きないのか、というと僕は全くそう思っていません。 私が考える共通点を2つ挙げてみます。

①エンジニアリングもマネジメントも課題解決である

実際のところ、ソフトウェア開発上の問題の多くは、技術的というより社会学的なものである

私の好きな ピープルウェア(トム・デマルコ) の一節です。現実的にはそんなことばかりです。マネジメントは社会学的な課題解決をしていくとも言えます。同時に、私が出会ってきたエンジニアの多くは、課題解決が大好きです。その時点でエンジニアはマネジメントに向いているのではないでしょうか。

②すべてがモデリング(設計)である

私はモデリングが大好きで、何をするときもモデルにして考えてしまうのですが、これはかなりマネジメントに活きているなと実感しています。

ドラッカーはマネジメントを

組織に成果をあげさせるための道具、機能、機関

と定義しています。道具、機能、機関を作ること自体、モデリング(設計)です。全体を俯瞰し、目標を作り、制度を作り、組織を作り、チームを作る。そこにどういうメッセージを投げかけ、働きかけて変化をつけていくかもモデリング。1on1で話すときは相手のメンタルモデルを理解して問いを投げかけます。だからマネジメントをしていく際には、常にモデリングだ!と思ってやるようにしています。

パワーアップしてエンジニアに戻ることができる

私自身はエンジニアリングマネージャーをやりつつ、コーディングも続けながらプロダクトマネジメントに足を踏み入れているのですが、私の知り合いでも エンジニア専任に戻った方 を何人も知っています。彼らを見ていて思うのは、パワーアップしてエンジニアをやっているなと感じます。例えばこんな部分です。

  • リーダーシップを発揮する経験をしたことで、マネージャーや経営層が何を考えて意思決定しているかその意図も読み取れる
  • マネジメントを経験したことで、今のチームで自分はどんな言動をすると成果につながるかを意識しながら動ける
  • 目の前のタスクや事象だけに惑わされず、全体を俯瞰した上で、最適な行動を選択できる
  • 言葉にして伝える大切さを学んだことで、チームとのコミュニケーションやコーディングに活きる

などなどです。マネージャーでの経験は確実にエンジニアでも活きます。面倒なことも多いことは否定しませんが、チームのパフォーマンスに大きく影響しますし、全然別の景色が見えるし、人間的にも成長させてもらえる価値ある役割だと思います。そして、マネージャーになってもエンジニアに戻れます。これは絶対。キャッチアップできます。

一方で、askenではスペシャリストのキャリアも用意しています

とはいえ、人には向き不向きや好き嫌いがあります。 エンジニアリングが好きで、エンジニアとして深く広く活躍できる方は、ずっとやっていたほうが良いに決まっています。なので、askenではスペシャリストでも活躍できるキャリアを用意しています。

興味のある方は是非 ご連絡 くださいませ。


  1. freeeさんやSansanさんで、マネージャー向けに今もコーチングをやらせていただいています

あすけんSlackの人気絵文字ランキング

はじめまして!
askenのユウマと申します😊

shoku-pan🍞という名前でTwitterをやっています。
askenでは、MLエンジニアとして働いてます。
主に、画像処理や自然言語処理、データサイエンス周りを担当しています。
また、業務効率化のためのツールの作成や、ナレッジ共有のための社内勉強会を開いたりもしてます。
社外では、kaggleやatmaCupに参加したり、データサイエンティストの方達ともくもく作業したりと、ゆるゆると活動しています。

さて、先日以下のTweetが弊社のエンジニアの間で話題になりました。

最近よく目にする(気がする)この動く棒グラフは、「bar chart race」と呼ばれるものらしいですね。
Slack APIで、社内Slackの絵文字データを集計し、データを可視化するツールを使えばできるようです!
僕自身、Slack APIを使った経験はあまりなく、「Slackのデータを色々と分析できたら面白いだろうな」と思っていました。

弊社askenのSlackでもやってみたので、この記事で紹介します😊

目次

  • はじめに
  • 目次
  • 全体の流れ
  • セットアップ
    • Slack Appの作成
  • 実装
  • Bar chart raceの表示
  • 🎉結果発表🎉
  • まとめ
  • 参考資料

全体の流れ

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

  • セットアップ
    • Slack Appの作成
  • 実装
    • Slackからメッセージ履歴を取得
    • 絵文字データを抽出・集計
    • csvファイルに出力
  • 可視化
    • Flourishにcsvファイルをインポート
    • bar chart raceの表示

セットアップ

Slack Appの作成

まず、Slackからデータを取得するために、Slack Appを作成します。 Slack Appの作成方法は、こちらの記事
Slack API | Web API から Slack App 経由で チャンネル履歴を取得する
を参考にしました。
ここで、Slack APIにどこまで権限を与えるか(スコープ)を決める必要があるのですが、今回は

  • channels:history
  • reactions:read

を指定しました。
※OAuth & Permissions > Scopes > User Token Scopes で設定できます。
スコープを指定してSlack APPを作成し、Slackのワークスペースにインストールできれば、いよいよ実装です!

実装

今回、言語はPython、環境はGoogle Colaboratoryを使用しました。
それでは、まずは設定です。

# Settings
TOKEN = '[token ID]'
CHANNEL = '[channel ID]'
NUMDAYS = 365 # データを取得する日数

ここで、[token ID]には
OAuth & Permissions > OAuth Tokens for Your Workspace > User OAuth Token
に記載されている文字列を指定します。
[channel ID]の確認方法は以下が参考になります。
Slack — APIに使う「チャンネルID」を取得する方法
※なお、これらは重要な情報なので、取り扱いに注意して下さい

NUMDAYSには、過去何日分のデータを取得するかを指定しています。
今回は1年分となる365日を指定しました。
※この値が大きいほど、データの取得には時間がかかります。

あらかじめ、必要なライブラリもインポートしておきます。

# Import libraries
import pandas as pd
import requests
import datetime

次に関数を定義します。

# Functions
def get_conversations_history(oldest_date, latest_date):
    """
    メッセージ履歴を取得する
    """
    url = "https://slack.com/api/conversations.history"
    headers = {"Authorization": "Bearer "+TOKEN}
    params = {
    "channel": CHANNEL,
    "limit": NUMDAYS,
    "oldest": oldest_date,
    "latest": latest_date,
    }
    response = requests.get(url, headers=headers, params=params)
    return response.json()
def count_emoji(conversations_history, df, date):
    """
    メッセージ履歴から絵文字をカウントする
    """
    for i in conversations_history['messages']:
        if 'ts' in i and 'reactions' in i:
            ts = i['ts']
            ts = datetime.datetime.fromtimestamp(int(ts.split('.')[0])) # unix時刻から標準時刻へ変換
            ts = ts.date() # 日付部分のみ取得
            reactions = i['reactions']
            for reaction in reactions:
                reaction_name = reaction['name']
                reaction_count = reaction['count']
                if reaction_name not in df.index:
                    df.loc[reaction_name] = 0
                df[date][df.index==reaction_name] += reaction_count

メインの処理は以下になります。

# Main
# 絵文字データをdataframeに格納
df=pd.DataFrame()
today = datetime.datetime.today()
# 今日の日付からNUMDAYS日前までのデータを取得。
# reversedで、日付の古い順に並び替え
for index, i in enumerate(reversed(range(0, NUMDAYS))):
    date_new = today - datetime.timedelta(days=i)
    date_old = today - datetime.timedelta(days=i+1)
    # Slack APIの時刻parameterはunix timeなので変換
    date_new_unix = date_new.timestamp()
    date_old_unix = date_old.timestamp()
    if index == 0:
        # df[date]に各絵文字の集計結果を格納する。カウント初日は0
        df[date_new.date()] = 0
    else:
        # df[date]に各絵文字の集計結果を格納する。前日の集計結果に足していく。
        df[date_new.date()] = df[date_old.date()]
        conversations_history = get_conversations_history(oldest_date=date_old_unix, latest_date=date_new_unix)
        if conversations_history['ok']: # responseが正しく返ってきた場合のみ処理実行
            count_emoji(conversations_history, df, date_new.date())
    
df # 結果確認

実行すると、以下のようなDataframeが出力されるはずです。
Slackの絵文字データがちゃんと集計されていますね!

f:id:techaskeninc:20210826174543p:plain

1日毎だとデータ量(カラム)が多すぎるので、1週間ごとに集計し直しました。
※カラムが多すぎると、この後Flourishにインポートしたときに全てのデータを表示することができませんでした。。

# Modify dataframe
df_result = df.copy()
df_weekly = pd.DataFrame()
for i in range(2, len(df_result.columns)):
    if i % 7 == 0:
        df_weekly[df_result.columns[i]] = df_result[df_result.columns[i]]
df_weekly

実行すると、以下のように出力されます。
今度は絵文字データが1週間単位(7日ごと)で集計されていますね!

f:id:techaskeninc:20210826174702p:plain

最後に、Dataframeをcsvファイルに出力しましょう。

# Output
df_weekly.to_csv('emoji_ranking_weekly.csv')

Bar chart raceの表示

ここまでで、Slackのデータを収集して絵文字データを集計し、結果をcsvファイルに出力するところまでできました。
では、このデータを使ってBar chart raceを表示してみましょう。
今回、Bar chart raceを表示するにあたっては、Flourishというサービスを使用しました。
こちらにアクセスしてください。
チュートリアルに従ってcsvファイルをupすれば、bar chart raceが表示されるハズです😉

🎉結果発表🎉

それでは、askenのSlackチャンネルの絵文字ランキングを発表します!
今回集計したデータは以下になります。

  • 全ての絵文字を集計(デフォルト絵文字およびカスタム絵文字)
  • generalチャンネル
  • リアクションとして使用された絵文字に限る(本文中で使用された絵文字は対象外)
  • 集計の期間は直近までの1年間
  • 上位15位を表示

直近1年間での集計の最終的なランキングは以下になりました🎉

順位 絵文字 絵文字名
🥇 tada
🥈 bows
🥉 done
4位 +1
5位 ok_hand
6位 ultra_fast_parrot
7位 asken2
8位 miki
9位 heart
10位 odaizini
11位 man-gesturing-ok
12位 woman-bowing
13位 nyan_parrot
14位 muscle
15位 sob

1位は🎉でした!新しいサービスのリリースやメディアでの紹介、また新入社員の紹介など、おめでたいことやテンションのあがる投稿に対するリアクションとして多く使われていました。
2位のは、「ありがとう」と感謝の気持ちをつたえる際に、3位のは依頼された作業を完了したときに使用されているようです。
また、7位には弊社サービス「あすけん」のロゴが、8位にはそのキャラクターの未来さんがランクインしていました。
これら2つはカスタム絵文字で、askenらしさがよく出た結果となりました😊

まとめ

いかがでしたか?

今回は、弊社askenのSlackの絵文字データを集計して、bar chart raceを表示してみました。
Slack APIを使用して、Pythonでデータを抽出・集計し、Flourishというサービスを使用することでつくることができます。

みなさんの会社でもやってみてはいかがでしょうか?
盛り上がること間違いなしだと思います!
ぜひ、皆さんの会社の絵文字ランキングも教えて下さいね

参考資料