「こんなグラフが見たいの!」と自然言語でお願いすれば出してくるものを作ってみた

「こんなグラフが見たいの!」と自然言語でお願いすれば出してくるものを作ってみた

こんにちは、小澤です。

われらが偉大なるTableau使いであるたまちゃんが以前書き記したTableauのAsk Dataという機能があります。

最近のAIってすごいですよね。 「どうやってこんな機能実現したんだろ?」と気になったので、似たような仕組みを実現しようとしてみました。 簡単なものだけの実装なので本家とは比べることもすらおこがましいくはありますが、 雰囲気だけを感じ取れる程度のものを紹介します。

前提条件

まず、どのようなことができるのかを決めます。 簡易的なものですので、作成できるグラフは以下のものに限定します。

  • ヒストグラム
  • 散布図
  • 折れ線グラフ
  • 棒グラフ

各グラフはカテゴリカルな値ごとに色分けするなどにも対応しません。 どのような文章が入力される想定かについてですが、以下のようなパターンを想定します。

  • <軸になる列>で<グラフの種類>
  • <軸になる列>と<軸になる列>で<グラフの種類>
  • <グループ化する列>で<集計方法>して<軸になる列>で<グラフの種類>
  • <軸になる列>を<グループ化する列>で<集計方法>して<グラフの種類>
  • <グループ化する列>で<集計方法>して<グラフの種類>

接続詞などはこの限りではないですが、項目の並びはこの順を想定しています。 また、グループ化の種類に関しては以下のものが使えることにします。

  • 合計
  • 平均
  • カウント

グラフの性質によってこれらすべての組み合わせパターンが実現可能なわけではないですが、入力の受付はこんな感じにします。

作ってみる

では、作ってみます。

  • テキスト入力されたものから必要な要素を抜き出す
  • それらの要素を使ってグラフを作成する

の2つのパーツを作成します。

テキスト入力されたものから必要な要素を抜き出す

まずはグラフの軸や種類などの必要な要素を抜き出す部分を実装します。 これを実現するために、PythonのJanomeというライブラリを使って形態素解析を行いました。

def target_extract(text):
    t = Tokenizer()
    # 入力された文章を形態素解析する
    tokens = t.tokenize(text)

    # 列名が形態素解析で分解されちゃうので名詞や記号が連続してるものはくっつける
    items = [tokens[0].surface]
    for index, token in enumerate(tokens[1:]) :
        # 品詞情報を取得
        hinshi, hinshi2 = token.part_of_speech.split(',')[0:2]
        # 品詞が名詞または記号で1つ前も同様であれば結合する
        if (hinshi == '名詞'or hinshi == '記号') and hinshi2 != '接尾' :
            pre_hinshi = tokens[index].part_of_speech.split(',')[0]
            if (pre_hinshi == '名詞' or pre_hinshi == '記号') and (hinshi == '名詞' or hinshi == '記号') :
                items[len(items)-1] = items[len(items)-1] + token.surface
            else:
                items.append(token.surface)
    return items

通常、列名は自由つけられるのもののため、単一の単語とは限りません。 必ずしもこのパターンで網羅できるわけではありませんが、名詞や記号の連続するものは結合しています。 より厳密にやるのであれば抜き出したものが列名やグラフの種類などのリストに含まれているかの確認なども必要になるでしょう。

今回は簡易的なもののため、そられの処理はスキップしていますが実際には以下のような要素も考慮したほうがいいでしょう。

  • 抜き出したものが列名になっているかのチェック
    • なっていない場合、前後の文字を使って補完する
  • 列名にtypoが含まれている場合の修正
  • 文章中に不要な単語が含まれている(先頭になぜか"Alexa"を入れちゃったなど)の削除

それらの要素を使ってグラフを作成する

抜き出した情報によって、列名やグラフの種類などの情報をリストで取得できました。 あとは、これをグラフ化します。 この部分はグラフの種類の数だけ愚直にif文で分岐させていきます。

def create_graph(target, df):
    # グラフの種類は一番最後に書いてある想定
    target_graph = target[-1]

    # 愚直にif文で頑張る
    if target_graph == 'ヒストグラム':
        # ヒストグラムの場合は<軸になる列>は1つのみ
        g = df[target[0]].plot.hist()
    elif target_graph == '散布':
        # 散布図の場合は軸になるx軸とy軸の列名が含まれる
        g = df.plot.scatter(x=target[0], y=target[1])
    elif target_graph == '折れ線グラフ':
        # 折れ線グラフも散布図と同様
        g = df.plot.line(x=target[0], y=target[1])
    elif target_graph == '棒グラフ':
        # 棒グラフの場合は、カテゴリカルな値ごとに集計した結果となる
        # 集計方法ごとにさらに分岐させる
        if '合計' in target:
            summarize_index = target.index('合計')
            group_value = target[summarize_index-1]
            # - <グループ化する列>で<集計方法>して<軸になる列>で<グラフの種類>
            # - <軸になる列>を<グループ化する列>で<集計方法>して<グラフの種類>
            # の2種類を想定しているので集計方法がどの位置にあるかで軸となる値とグループ化対象を選択している
            if summarize_index == 1:
                axis_item = target[summarize_index+1]
            else :
                axis_item = target[0]
            g = df.groupby(group_value)[axis_item].sum().plot.bar()
        elif '平均' in target:
            # 平均はグラフ作成時にsumを使うかmeanを使うかの違いのみなので処理がほぼ重複している
            # 本来であれば関数化などを検討
            summarize_index = target.index('平均')
            group_value = target[summarize_index-1]
            if summarize_index == 1:
                axis_item = target[summarize_index+1]
            else :
                axis_item = target[0]
            g = df.groupby(group_value)[axis_item].mean().plot.bar()
        elif 'カウント' in target:
            # データ件数のカウントの場合グループ化する列のみを指定する
            summarize_index = target.index('カウント')
            axis_item = target[summarize_index-1]
            g = df.groupby(axis_item)[axis_item].count().plot.bar()
    return g

こちらは分岐が多いため長くなっていますが、複雑な処理はしていません。

実際の動きを見てみる

いくつかの文章を与えて実際の動きを見てみましょう。 まずは、以下のようなデータを読み込みます。

これに対していくつかのグラフを作成します。

これだけだと胡散臭いので他のデータでもやってみましょう。

できないこと

この仕組みのままではできないことも多くあります。

まず、"明示しなくてもデータから適切なグラフを自動選択可能な場面"というのがあります。 これは(自然言語関係なく)、Tableauなどでも軸のになる項目を入れるとグラフの種類を明示しなくても作ってくれたりしますね。 なので、軸になる値さえ取得できれば明示しなくてもいい場面というのは考えられます。

続いて、前後に不要な情報が含まれているような場面です。 これはたとえは、最初に間違って「Alexa、」と書いてしまうような状況や、末尾に「~を作成」のように作りたいグラフの要素とは関係ない名詞が含まれている場合の動作を考えて作っていません。

また、"<グループ化する列>で<軸になる列>を<集計方法>して<グラフの種類>"のような想定していない並びにも対応していません。 今回は、単語のみを抜き出しているため、どれがどれに係っているかの判断が付きません。 そのため、そういった要素を含めるか、単語を抜き出す際に係り受け解析の結果で並び替えるなどの処理が必要になるでしょう。

そのほか、列名はデータ作成者が自由につけることが可能な部分なので、工夫が必要です。 これは都度与えられるものなので形態素解析の辞書登録などは難しいですが、変数化して対応関係で置換してしまうなどが考えられます。 今回はそこまでやっていません。

最後に、より高度な文脈を与えることはできませんので、項目などは明示する必要があります。 これは例えば「月ごとの売上」のような、「月」や「売上」の計算対象となる列がどれに対応しているかはAIが自動で判断してくれたりはしません。 そのため、より高度な「KPIの表示」みたいなことも可能とはなっていません。

おわりに

TableauのAsk Dataすげー!と思ったので、似たような仕組みが実現できないか腕試しをしてみました。 今回は、比較的単純な手法のみで実現していますが、より高度な手法を使ってもっと複雑なことまでできるようにならないかやってみたいですね。