astamuse Lab

astamuse Labとは、アスタミューゼのエンジニアとデザイナーのブログです。アスタミューゼの事業・サービスを支えている知識と舞台裏の今を発信しています。

技術的負債と戦う指標の取り方と改善の仕方、そして諦め方

どうも、えいやです。

今回もお鉢が、というかぬいぐるみが回ってきたので、ブログを書きます。

今回は、技術的負債の計測と返済についてです。こんな方向で適当にやってるというだけの話とポエムなので、真面目に話を聞きたい人は、品質管理の専門家に相談してください。

なお、今回もJavaで開発を行っている想定での話です。あと、最後はやっぱり暗い気持ちで締めることになるので、誰か手伝いに来てください。

SonarQubeの紹介

僕がメインでメンテナンスしているサービスでは、CIに組み込んだSonarQubeを用いて静的解析ツールや動的テスト結果の解析ツールを実行し、CodeSmellなどのメトリクスを集計、可視化しています。

技術的負債とは、それらのメトリクスの個々の理由うちコード修正が必要なものについて、修正に要する日数を算定し、コストとして積み上げたものです。

www.sonarqube.org

SonarQubeについての詳しい内容は、公式のサイトを見てください。多様なプログラミング言語について指標を出せますが、解析ツールが洗練されているJavaについての指標がもっともきちんと提示できるようです。

Astamuse.comの場合

弊社の主要なサービスでは、Astamuse.com、AstaID.comについて取得しています。また、それらを構成するライブラリについても、自製のものについては取得しています。

例えば、コードベースが大きいAstamuse.comでは、Java言語で実装されている部分について、現在のメトリクスは以下の数値となっています。

         Lines of Code   Bugs    Vulnerabilities Code Smells Coverage    Duplications
main-java   42k             49      20              1.4k        30.4%       4.9%

これらの指標に基いて算出された技術的負債は62日となっています。これが多いのか少ないのかは、プロジェクトによって異なると思いますので言及しません。

ここ2年の技術的負債の推移を見てみると以下のようになります。

f:id:astamuse:20171023180246p:plain
技術的負債推移グラフ

新機能の追加で負債が増加することもあり、二年前の時点の167日を完済とは行きませんが、メインコードについては負債は減少傾向にあります。

借金で例えると「新規の借り入れ」はありつつも「元本」を返せている感じがしますね。まぁ頑張っている方だと思います。

数値の改善に取り組む

さて、この返済をどう行ってきたのかについてです。

もちろん、基本的には、個別の指摘事項を潰していくことで返済していますが、指摘されたことを機械的にやっていたのでは埒があきません。

以下では一気に改善してみせる方法を取り上げます。といっても、どれも当たり前な改善方法ですが。

抽象設計を行う

まず、負債が大きいコードでは設計がなっていない事が多いです。

コードをみて、オブジェクト指向の基礎にしたがった設計をしっかりと行います。

また、既存のコードが増える度に、抽象化して共通化すべき概念は増えていくはずです。エリック・エヴァンスが好きならそれらの一部はドメインと言い換えても良いでしょう。

手続きとしての共通化ではなく、抽象度の高い設計を行い、コードを共通化しましょう。

抽象設計が正しく行えると、コードの重複が少なくなりますので、わりと劇的に指標が改善します。

とくに、Java8以降では、ラムダ式やdefaultメソッドが使えるので、Java7以前から存在しているコードについては見直せば大きな効果が期待できるでしょう。

テストで使用されているMockの見直し

単体テストが設計時のままの場合、テストの対象となっているクラスと連携する他のクラスでモックを使用しているケースが多いかもしれません。

それらがインスタンス化が出来るなら、インスタンス化してしまいましょう。無理ならスパイを使っても良いでしょう。

単純にカバレッジが改善することもあれば、実際にはどうやったって通ることのない不要なコードが見つかって、それを削除できたりします。

例えば、Nullになり得ないシーンでのNullチェックや、@NonNullアノテーションをつけられるシンボルの発見などです。

ライブラリへの切り出しでよく見せる

指標を改善するだけなら最もよく見せる方法がこれです。

じつは、グラフ中に現れる大きな減少は、メインから別のライブラリへコードを切り出した結果としてメインコードから負債が消えたという部分があります。

むろん、その場合では、ライブラリの負債も合計すれば負債総額は変わらないです。いわば(メインからは)視えない化ですね。

それでも、メインコードの負債が減るのは短期的には良い傾向だと捉えられます。

なぜなら、一般にライブラリへのコードの追加・更新はメインコードへの追加更新よりも頻度が低いからです。このことをまた借金で例えるなら、返済期限が長く、新規の借り入れをあまり必要としない返済方法への乗り換えと捉える事ができます。

また、ライブラリ化によってコードの独立性が高まることで、そのライブラリに関する業務を分離することが出来ます。メンバーが増えたときにその業務を担当させやすくなるでしょう。借金の肩代わりをさせるようなものといえます。

なお、ライブラリ化できる構造に変更すること自体が、設計を正しくし、指標の改善となる傾向があります。

もちろん、機能をライブラリとして切り出すべきかどうか、きちんと考えた上で行う必要がありますので、なんでも分割すればいいというわけではないことには注意しましょう。メイン部分と独立している、改修の頻度が高くない機能が切り出す対象として向いています。

そして、切り出した後のライブラリを知らんぷりしていると、多重債務者状態になって立ち行かなくなることも心に留めておきましょう。

視えない指標にも対抗する。。。けど心構えは必要

上記までは、基本的に視えている技術的負債に対抗する方法です。

技術的に負債には、見えない部分があって、それらについても対抗しておかないと困ったことなります。

陳腐化に対抗する。。。のにも限界はある

さて、視えない部分で大きな部分を占めるのが陳腐化による内在的な負債です。

陳腐化とは、時間経過により使用する基盤技術やライブラリが古くなってしまい、刷新を行わなければならなくなることを指します。もちろんコストがかかります。

いつライブラリや環境が新バージョンをリリースするか、旧バージョンのメンテナンス期限が切れるかなどは、その全てを自動で把握することが難しいため、SonarQubeでも指標として取りにくいケースが多いです。

セキュリティ上の問題が発覚したりなどは、ある日突然に起こる場合もあります。

使用している技術が突如として使用不能になったり、採用した技術の新しいバージョンへの移行が極端に難しいことが判明してしまう様子は、借金に例えるなら、貸し剥がしや貸し渋りといったところでしょうか。

可能な限り最新のライブラリ環境に寄せ続けることでリスクを減らすことは出来ますが、なかなかに難しいことが多いでしょう。

対抗策として考えられるのは、設計段階で腐敗防止層を設けたり、ファサードのような実装が可換なパターンを用いていたりなどですが、それらはライブラリの変更には耐えられても、言語のアップグレードなど基盤技術の変更には耐えられません。

どんなシステムでも、陳腐化の原因については対策は後手に回る以外にないため、「陳腐化速度>メンテナンス速度」が宿命づけられており、いずれ陳腐化による借金が返済不能になると思っています。

何処かでしっかりと諦めて、システムの一からの作り直しをするか、もう止めてしまう覚悟が必要だと思います。

ちなみに、今のAstamuse.comはJava製ですが、かつて黎明期にあったScalaを用いて、同じく初期頃のバージョンのLiftで組まれていた時期があります。乗り換えの理由は、総合的な理由であり、陳腐化のみではなかったかと思います。昔のScalaは、今よりも問題が多かったことは確かです。

前提条件の変更もある。。。流れに乗らざるを得ない

陳腐化と似ているのですが、時代の変化により前提の条件が変更されるということもあります。時間というより、もっと大局的な時代でおきる、いわゆる時代の要請ですね。

条件の変更の例を言えば、CPUの進化や、メモリ容量の向上、クラウドの普及、スマートフォンユーザの増加、HTTPSデフォルトの一般化、などです。

それらを理由に、システムの根本的な作りや、データの取り扱い方、必須とされる機能の要件など、様々なところで変更が強いられます。

つまり、かつての時代の常識が、今の時代の非常識になったため、対応を取らなければならないことですが、Webサービスで言えば、FlashからHTML5への刷新などですね。

こうした内容は、陳腐化と同様にSonarQubeでは指標化出来ないこともありますので、他の方法で気にかけられるようにしておくべきでしょう。たとえばモバイルフレンドリー調査ツールやアクセス解析など、要件ごとの解析ツールなどです。

Astamuse.comでも、こうしたツールを用いて、細かな修正で対応可能な範囲においては、時代の要請にあった改修を続けています。

もちろん、小さな変更では済まないことも多くあります。例として出したFlashの廃止も、手法によってはそのうちの一つとなるでしょう。

Astamuse.comにおいてそのような部分の一つに、コンポーネント化への対応というのがあります。

Astamuse.comの基本的機能は、かつてGoogleの検索ボットが動的ページのインデックスをうまく行えなかった時代に作られています。それゆえ、Astamuse.comは、データを元に静的なHTMLを生成できるCMSのような実装が得意なFWを用いて、サーバサイドでHTMLをレンダリングするシステムとして組まれています。

ですが、ご存知の通り、今ではGoogleのBotは賢く、フロントエンドで描画されるインタラクティブなページも難なく解析し、検索インデックスに加えることが出来ます。

一般に、サーバサイドレンダリングは、サーバサイドへの負荷が高く、クライアントでも重複するHTMLデータを何度もダウンロードをしなければならないため、非効率とされています。

なので、いつとは言えませんがAstamuse.comもそのようなサーバサイドレンダリングからフロントエンドレンダリングに実装を切り替えていくことになるでしょう。

こうした改修は、コストが大きいので、既存のシステムを「諦める」可能性についても考える必要があります。

今のAstamuse.comのコードベース全てを諦めるということはないと思いますが、ミドルウェアやフレームワークの選択を含め今後考えていくことになるでしょう。割りと好き勝手に。

まとめ

  • 指標を見えるようにしよう。
  • 指標を改善する設計を行おう。
  • 一度書いたテストも放っておかずにメンテしよう。
  • メインコードから切り出すべき機能を探してライブラリにしておこう。
  • 見えない部分にも気を配ろう。
  • 諦めるときは諦めよう。
  • 諦めたら割りと好き勝手出来ると思う。

P.S で、Java9もう出てるよね

先日、待望のJava9が発表され、モジュール化機能の検証をせざるを得ないなぁと思っているところにその後のJavaのリリース計画も半年ごとに云々とか、、、

新しいことが出来るようになっていくのはとてもいいことなんだけど、抱え込んでるものの大きさにもよるよなぁ。

全くもって、人手が足りません。

Vue.jsとYQLでお手軽RSSフィード実装

先日、弊社採用サイトに本ブログのRSSを表示させる機能を追加したのですが、その実装がとてもお手軽だったので共有します。

YQL API Consoleのざっくりトリセツ

昨年から定番として使われてきたgoogleのAPIが廃止になったので、代替としてYQL APIを使います。 RSSの取得、確認にはYQL API Consoleが便利です。 左サイドバーの各リンクをクリックすると、それぞれ対応したサンプルのステートメントが表示されるので、ここではData→RSSをクリック。

select * from rss where url='http://lab.astamuse.co.jp/rss/category/'

'url'の箇所に本ブログのURLをいれてtestすると下記のようなjsonが返ってきます。

{
  "query": {
    "count": 30,
    "created": "2017-10-18T02:38:53Z",
    "lang": "en-US",
    "diagnostics": {
      "publiclyCallable": "true",
      "url": {
        "execution-start-time": "1",
        "execution-stop-time": "1112",
        "execution-time": "1111",
        "content": "http://lab.astamuse.co.jp/rss/category/"
      },
      "user-time": "1130",
      "service-time": "1111",
      "build-version": "2.0.187"
    },
    "results": {
      "item": [
        {
          "title": "データクレンジングとかクォリティチェックとかの話",
          "link": "http://lab.astamuse.co.jp/entry/2017/10/11/114500",
          "description": "<p>いつもご覧いただき誠にありがとうございます。",
          "pubDate": "Wed, 11 Oct 2017 11:45:00 +0900",
          "guid": {
            "isPermalink": "false",
            "content": "hatenablog://entry/8599973812305032728"
          },
          "category": [
            "Big Data",
            "データ開発エンジニア",
            "データ分析",
            "データクレンジング",
            "Data preparation"
          ],
          "enclosure": {
            "length": "0",
            "type": "image/png",
            "url": "https://cdn-ak.f.st-hatena.com/images/fotolife/a/astamuse/20171006/20171006150137.png"
          }
        }
      ]
    }
  }
}

パパっと整形

今回フィードリストで表示させたいのは

  • タイトル
  • 日付
  • 記事へのリンク
  • 記事のサムネイル画像

なので 、

select * from rss where url='http://lab.astamuse.co.jp/rss/category/'

こちらのワイルドカードになっている箇所に

title,link,pubDate,enclosure.url

をいれて、さらに、最新の4件だけにしたいので、rssの箇所をrss(4)に変更し再度テストすると下記のようなデータが返ってくる。

{
 "query": {
  "count": 4,
  "created": "2017-10-18T03:17:40Z",
  "lang": "en-US",
  "diagnostics": {
   "publiclyCallable": "true",
   "url": {
    "execution-start-time": "1",
    "execution-stop-time": "995",
    "execution-time": "994",
    "content": "http://lab.astamuse.co.jp/rss/category/"
   },
   "user-time": "1010",
   "service-time": "994",
   "build-version": "2.0.187"
  },
  "results": {
   "item": [
    {
     "title": "データクレンジングとかクォリティチェックとかの話",
     "link": "http://lab.astamuse.co.jp/entry/2017/10/11/114500",
     "pubDate": "Wed, 11 Oct 2017 11:45:00 +0900",
     "enclosure": {
      "url": "https://cdn-ak.f.st-hatena.com/images/fotolife/a/astamuse/20171006/20171006150137.png"
     }
    },
    {
     "title": "Spark3分クッキング HBaseで作る100万通りの文書分類器",
     "link": "http://lab.astamuse.co.jp/entry/2017/10/04/114500",
     "pubDate": "Wed, 04 Oct 2017 11:45:00 +0900",
     "enclosure": {
      "url": "https://cdn-ak.f.st-hatena.com/images/fotolife/a/astamuse/20171004/20171004104538.jpg"
     }
    },
    {
     "title": "データドリブンな企業とは何か~アスタミューゼ流宴会術~",
     "link": "http://lab.astamuse.co.jp/entry/data-driven-company",
     "pubDate": "Wed, 27 Sep 2017 11:50:00 +0900",
     "enclosure": {
      "url": "https://cdn-ak.f.st-hatena.com/images/fotolife/a/astamuse/20170925/20170925115807.png"
     }
    },
    {
     "title": "デザインの良し悪しを語るのに必要な「分解」について【書体編】",
     "link": "http://lab.astamuse.co.jp/entry/2017/09/20/120000",
     "pubDate": "Wed, 20 Sep 2017 12:00:00 +0900",
     "enclosure": {
      "url": "https://cdn-ak.f.st-hatena.com/images/fotolife/a/astamuse/20170920/20170920112706.png"
     }
    }
   ]
  }
 }
}

これでYQLのほうの準備は完了。URLはページ下にある「THE REST QUERY」の箇所に生成されます。


https://query.yahooapis.com/v1/public/yql?q=select%20title%2C%20pubDate%2C%20link%2C%20enclosure.url%20from%20rss(4)%20where%20%0A%20%20url%3D'http%3A%2F%2Flab.astamuse.co.jp%2Frss%2Fcategory%2F'&format=json&diagnostics=true&callback=

 Vue.jsで実装

今回Vueでajaxを扱うためのライブラリはvue-resoueceを使用しています。

採用サイトでは各ページに対応した職種(カテゴリー)のブログ記事を表示させたかったので、カスタムコンポーネントのpropsにカテゴリー名を入れて各カテゴリ一覧ページを読みにいかせるようにしました。

設置例)

<v-gethatena category="webデザイナー"></v-gethatena>

webデザイナーのカテゴリ一覧ページ

ソース例

templates

<div>  
  <ul>
    <li v-for="item in items">
      <a :href="item.link" target="_blank">
        <div class="feed-thumbnail"><img :src="item.enclosure.url"></div>
        <div class="feed-header">
          <span class="feed-date">{{item.pubDate}}</span>
          <h3 class="feed-title">{{item.title}}</h3>
        </div>
      </a>
    </li>
  </ul>
</div>

js


data() {
  return {
    items: [],
    isLoading: true 
  };
},
props:{
  category : { required: true }
},
created: function(){
  let yql = "https://query.yahooapis.com/v1/public/yql?q=select%20title%2Clink%2Cdescription%2CpubDate%2Cenclosure.url%20from%20rss(4)%20where%20url%3D";
  let lab = "http://lab.astamuse.co.jp/rss/category/";
  let requestURL = yql + "'" + lab + this.category+ "'" + "&format=json&diagnostics=true&callback="

  this.$http.get(requestURL).then(response => {
    for (var i = 0; i < response.body.query.results.item.length; i++) {
      this.items = response.body.query.results.item;
    }
  }, response => {
    // error callback
  });
}

これだけでとりあえずRSSフィードは実装完了です! (採用サイトで実装している実際のソースは最後に書いておきます。違いはローディングやデータフォーマットくらいですが。。。)

ホントお手軽にRSSフィード実装できちゃうので、まだ触ったことない人はぜひVue.jsとYQL触ってみてください~ (ただ、YQLはこの前2,3週間ほどサービスダウンしてたので、大事な箇所に利用するのはちょっと怖いw)


Vue.component('v-gethatena', {
  template: `
  <div>  
    <div v-if="isLoading">
      <slot></slot>
    </div>
    <div v-else>
      <transition-group name="v-gethatena" tag="ul" class="mod-list-feed" appear
      @before-enter="beforeEnter"
      @after-enter="afterEnter"
      @enter-cancelled="afterEnter">
        <li v-for="(item, index) in items" :key="index" :data-index="index">
          <a :href="item.link" target="_blank">
            <div class="feed-thumbnail"><img :src="item.enclosure.url"></div>
            <div class="feed-header">
              <span class="feed-date">{{item.pubDate}}</span>
              <h3 class="feed-title">{{item.title}}</h3>
            </div>
          </a>
        </li>
      </transition-group>
   </div>
  </div>
  `, 
  data() {
    return {
      items: [],
      isLoading: true 
    };
  },
  props:{
    category : { required: true }
  },
  created: function(){
    let yql = "https://query.yahooapis.com/v1/public/yql?q=select%20title%2Clink%2Cdescription%2CpubDate%2Cenclosure.url%20from%20rss(4)%20where%20url%3D";
    let lab = "http://lab.astamuse.co.jp/rss/category/";
    let requestURL = yql + "'" + lab + this.category+ "'" + "&format=json&diagnostics=true&callback="

    var 
    this.$http.get(requestURL).then(response => {
      for (var i = 0; i < response.body.query.results.item.length; i++) {
        this.items = response.body.query.results.item;
        this.items[i].pubDate = this.dateFormat(this.items[i].pubDate);
      }
      setTimeout(function() {
        this.isLoading = false;
      }.bind(this), 1500);

    }, response => {
      // error callback
    });

  },
  methods: {
    beforeEnter: function(el) {
      el.style.transitionDelay = 250 * el.dataset.index + 'ms'
    },
    afterEnter: function(el) {
      el.style.transitionDelay = ''
    },
    dateFormat: function(str){
      var my_date = new Date(str);
      var year = my_date.getFullYear();
      var month = my_date.getMonth() + 1;
      var date = my_date.getDate();
      var format_date = year + "/" + month + "/" + date;
      return format_date;
    };
  }  
});

new Vue({
    el: '#v-root'
});

データクレンジングとかクォリティチェックとかの話

いつもご覧いただき誠にありがとうございます。

ご存知のとおり?弊社は世界最大級のイノベーションデータベースを保有しており、中にはクラウドファンディング、科研費データ、特許データなど様々なデータが含まれてます。 普段仕事上データを入手してから弊社DBに入れるまでのプロセス(所謂Data Preparation的な?)を担当するので、今日はその辺で役たちそうなネタにしようかと思います。

データの種類が多い分、それぞれ元データの入手ルートによってはノイズが含まれていることもございます。 例えば科研データはクォリティが高いのですが、クラウドファンディングはデータの性質(個人もプロジェクトを起こすことが出来る)上どうしてもノイズが含まれたりする場合があります。
そのようなデータを弊社データベースに入れる前にデータの品質管理をきちんと行い、ノイズを排除する必要があります。

その為のツールとしてデータクリーニング系のオープンソースを探してますが、現状オープンソースの製品がそれほど多くないのが実情であって私自身もどれを活用すればいいのか結構迷ってます。
そこで今日はデータクォリティをチェックできるオープンソースのツール:Optimusを例を交えながら紹介したいと思います。

Optimus

Spark(PySpark)と連携してデータのクォリティをチェック、データクレンジング、変換ができるツールです。 正式リリースして間もないので、機能面でまだこれからという感じではありますが、
この類のopen sourceがまだまだ少ないので今回試してみることになりました。
Github : https://github.com/ironmussa/Optimus

事前準備

  • 環境

    • Apache Spark 2.2
    • Python >= 3.5
  • pipからインストール

pip install optimuspyspark 


  • Spark packageとして利用する
    ※まず、python 3.5以上入れる必要があります。
export PYSPARK_PYTHON=python3 // Pysparkのpythonバージョンを3.5以上に設定する
pyspark2 --packages ironmussa:Optimus:1.0.3


  • 作業の便宜上jupyterも入れて、起動する
pip3 install jupyter
jupyter notebook --allow-root --ip='*'


動かしてみる

基本的にサンプルはオフィシャルサイトに載ってありますが、そのままだと動かなかったので(たぶんドキュメントが古い?) ちょっとhelpで仕様探ったりしてました。

今回はクラウドファンディングのデータを用意して色々試してみたいと思います。

以下データ一部

+----------+--------------------+-----------+--------------------+-----------+-------------+-------------+
|project_id|                name|   category|            location|goal_amount|raised_amount|backers_count|
+----------+--------------------+-----------+--------------------+-----------+-------------+-------------+
|      4876|  Spirit of the Game|       Film|London, Stoke, Bl...|       5000|          335|            5|
|   1006067|           FoodQuest|       Food|Wiener Neustadt, ...|      25000|           25|            2|
|       814|           Connected|       Film|San Francisco, Ca...|      10000|          112|            4|
|     53500|            Arcadian|       Film|Orlando, Florida,...|       1500|          400|            5|
|    339731|Help me build out...|Photography|Austell, Georgia,...|       5000|          115|            4|
|    380670|The Improvisation...|      Music|Portland, Oregon,...|       2000|         1689|           35|
|   1932270|    The Lonely Angel|       Film|San Jose, Califor...|       5000|         2115|           27|
|   1300172|      The House Lady|  Community|Knoxville, Tennes...|      15000|           58|            7|
|     42509|     Mindless Series|Video / Web|Central Coast NSW...|        500|          130|            3|
|    814777|   Spencer Zahrn Art|        Art|Little Rock, Arka...|       1200|         1400|           16|
+----------+--------------------+-----------+--------------------+-----------+-------------+-------------+

ではまず上記のデータの統計情報(stats的な)を取ってみたいと思います。

jupyter起動して以下のコマンドを打ちます

import optimus as op
import os

console上以下のメッセージが出るまで待ちます

Starting or getting SparkSession and SparkContext.
Setting checkpoint folder (local). If you are in a cluster change it with set_check_point_folder(path,'hadoop').
Deleting previous folder if exists...
Creation of checkpoint directory...
Done.

テストデータを読み込んで全カラムの分析を行う

tools = op.Utilities()
filePath = "file:///" + os.getcwd() + "/test/cf_sample.csv"
df = tools.read_dataset_csv(path=filePath,delimiter_mark=',')
analyzer = op.DataFrameAnalyzer(df=df,path_file=filePath)
analyzer.column_analyze("*", plots=False, values_bar=True, print_type=False, num_bars=10) // 全カラムの分析を実施

全カラムの分析を実施すると下記結果のように各カラムごとデータ型毎の件数、Min valueとMax valueなどが出力されることが分かります。
※'*'の代わりに分析したいカラム名だけ書けばそのカラムだけ分析できます。

結果一部

f:id:astamuse:20171006150137p:plain

上記の結果からgoal_amountのカラムにはintegerとstringタイプが混在していて、nullの値も含まれていることが分かります。

続いてカラムごとの詳細分析:値の出現頻度を調べます。

categoryDf = analyzer.get_data_frame().select("category") // 詳細分析したいカラム
hist_dictCategory = analyzer.get_categorical_hist(df_one_col=categoryDf, num_bars=10) //文字列ごとの出現頻度
print(hist_dictCategory)

結果

[{'value': 'Film', 'cont': 215}, {'value': 'Community', 'cont': 127}, {'value': 'Music', 'cont': 102}, {'value': 'Education', 'cont': 72}, {'value': 'Health', 'cont': 55}, {'value': 'Small Business', 'cont': 49}, {'value': 'Theatre', 'cont': 48}, {'value': 'Technology', 'cont': 42}, {'value': 'Video / Web', 'cont': 36}, {'value': 'Art', 'cont': 33}]

以下のコードでグラフ化もできます

analyzer.plot_hist(df_one_col=categoryDf,hist_dict= hist_dictCategory, type_hist='categorical')

f:id:astamuse:20171006154229p:plain

カラムごとのユニーク値をカウント

print(analyzer.unique_values_col("location"))

結果

{'total': 1000, 'unique': 555}

上記以外にも色んな機能が使えますので、詳細はhelp(analyzer)でご確認ください

データ変換

データ変換はDataFrameTransformerクラスで提供してます

主には以下の機能が提供されてます

  • 対カラム(column):

    • カラムのdrop
    • replace
    • rename
    • move(ポジション変更)
  • 対行(Row):

    • 行のdrop
    • delete
  • 個別のカラムに対して:

    • trim
    • accentのクリア
    • lookup
    • 特殊文字の削除
    • 日付のフォーマット変換

詳細はGithubにありますので、 ここでは一部分だけピックアップして説明したいと思います

transformer = op.DataFrameTransformer(df)
transformer.show()

+----------+--------------------+-----------+--------------------+-----------+-------------+-------------+
|project_id|                name|   category|            location|goal_amount|raised_amount|backers_count|
+----------+--------------------+-----------+--------------------+-----------+-------------+-------------+
|      4876|  Spirit of the Game|       Film|London, Stoke, Bl...|       5000|          335|            5|
|   1006067|           FoodQuest|       Food|Wiener Neustadt, ...|      25000|           25|            2|
|       814|           Connected|       Film|San Francisco, Ca...|      10000|          112|            4|
|     53500|            Arcadian|       Film|Orlando, Florida,...|       1500|          400|            5|
|    339731|Help me build out...|Photography|Austell, Georgia,...|       5000|          115|            4|
|    380670|The Improvisation...|      Music|Portland, Oregon,...|       2000|         1689|           35|
|   1932270|    The Lonely Angel|       Film|San Jose, Califor...|       5000|         2115|           27|
|   1300172|      The House Lady|  Community|Knoxville, Tennes...|      15000|           58|            7|
|     42509|     Mindless Series|Video / Web|Central Coast NSW...|        500|          130|            3|
|    814777|   Spencer Zahrn Art|        Art|Little Rock, Arka...|       1200|         1400|           16|
+----------+--------------------+-----------+--------------------+-----------+-------------+-------------+


以下の例ではreplace_colを使ってcategoryのカラムからFilmの文字列をMovieに置換します。

transformer.replace_col(search='Film', change_to='Movie', columns='category')
<optimus.df_transformer.DataFrameTransformer at 0x7f7243ae0cc0> // エラーっぽいのが出てるけど処理自体は想定とおり動いたので、とりあえず気にしません

transformer.show() // 変更後
+----------+--------------------+-----------+--------------------+-----------+-------------+-------------+
|project_id|                name|   category|            location|goal_amount|raised_amount|backers_count|
+----------+--------------------+-----------+--------------------+-----------+-------------+-------------+
|      4876|  Spirit of the Game|      Movie|London, Stoke, Bl...|       5000|          335|            5|
|   1006067|           FoodQuest|       Food|Wiener Neustadt, ...|      25000|           25|            2|
|       814|           Connected|      Movie|San Francisco, Ca...|      10000|          112|            4|
|     53500|            Arcadian|      Movie|Orlando, Florida,...|       1500|          400|            5|
|    339731|Help me build out...|Photography|Austell, Georgia,...|       5000|          115|            4|
|    380670|The Improvisation...|      Music|Portland, Oregon,...|       2000|         1689|           35|
|   1932270|    The Lonely Angel|      Movie|San Jose, Califor...|       5000|         2115|           27|
|   1300172|      The House Lady|  Community|Knoxville, Tennes...|      15000|           58|            7|
|     42509|     Mindless Series|Video / Web|Central Coast NSW...|        500|          130|            3|
|    814777|   Spencer Zahrn Art|        Art|Little Rock, Arka...|       1200|         1400|           16|
+----------+--------------------+-----------+--------------------+-----------+-------------+-------------+


以下の例ではTransformer.delete_row(func)をもちいて条件に合致するレコードを削除する

from pyspark.sql.functions import col
func = lambda g: (g > 5000) & (g <= 200000) //残したい条件を設定する
transformer.delete_row(func(col('goal_amount')))

ここではgoal_amountが5000より大きく、200000以下のレコードを残すようにしたので、結果はこんな感じになります

+----------+--------------------+--------------+--------------------+-----------+-------------+-------------+
|project_id|                name|      category|            location|goal_amount|raised_amount|backers_count|
+----------+--------------------+--------------+--------------------+-----------+-------------+-------------+
|   1006067|           FoodQuest|          Food|Wiener Neustadt, ...|      25000|           25|            2|
|       814|           Connected|         Movie|San Francisco, Ca...|      10000|          112|            4|
|   1300172|      The House Lady|     Community|Knoxville, Tennes...|      15000|           58|            7|
|    783762|A Story Unwritten...|         Movie|    Barcelona, Spain|       7500|         7925|          127|
|    781986|Die deutsch - chi...|     Education|     Berlin, Germany|      12000|          450|            5|
|    475016|6:38: THE DEATH O...|         Movie|Los Angeles, Cali...|     200000|          390|            7|
|    370437|            SLOW WIN|         Movie|Toronto, Ontario,...|      10000|          740|           19|
|     53835|The Finger and th...|           Art|        Genoa, Italy|      15000|          150|            4|
|    513792|Intrusion Disconn...|         Movie|Louisville, Kentu...|      10000|          195|            4|
|   1188584|Wild West Steel -...|Small Business|Idaho Falls, Idah...|      50000|           30|            2|
+----------+--------------------+--------------+--------------------+-----------+-------------+-------------+


Transformer.set_col(columns, func, dataType)を使ってセルの変換を行う

func = lambda backers_count: 10 if (backers_count < 10 ) else backers_count
transformer.set_col(['backers_count'], func, 'integer')


10より小さいbackers_countの値を10に変換するので、以下のように変換されます

+----------+--------------------+--------------+--------------------+-----------+-------------+-------------+
|project_id|                name|      category|            location|goal_amount|raised_amount|backers_count|
+----------+--------------------+--------------+--------------------+-----------+-------------+-------------+
|   1006067|           FoodQuest|          Food|Wiener Neustadt, ...|      25000|           25|           10|
|       814|           Connected|         Movie|San Francisco, Ca...|      10000|          112|           10|
|   1300172|      The House Lady|     Community|Knoxville, Tennes...|      15000|           58|           10|
|    783762|A Story Unwritten...|         Movie|    Barcelona, Spain|       7500|         7925|          127|
|    781986|Die deutsch - chi...|     Education|     Berlin, Germany|      12000|          450|           10|
|    475016|6:38: THE DEATH O...|         Movie|Los Angeles, Cali...|     200000|          390|           10|
|    370437|            SLOW WIN|         Movie|Toronto, Ontario,...|      10000|          740|           19|
|     53835|The Finger and th...|           Art|        Genoa, Italy|      15000|          150|           10|
|    513792|Intrusion Disconn...|         Movie|Louisville, Kentu...|      10000|          195|           10|
|   1188584|Wild West Steel -...|Small Business|Idaho Falls, Idah...|      50000|           30|           10|
+----------+--------------------+--------------+--------------------+-----------+-------------+-------------+


次はcategoryをすべて大文字に変換することを試します

func = lambda category: category.upper()
transformer.set_col(['category'], func, 'string')


結果

+----------+--------------------+--------------+--------------------+-----------+-------------+-------------+
|project_id|                name|      category|            location|goal_amount|raised_amount|backers_count|
+----------+--------------------+--------------+--------------------+-----------+-------------+-------------+
|   1006067|           FoodQuest|          FOOD|Wiener Neustadt, ...|      25000|           25|           10|
|       814|           Connected|         MOVIE|San Francisco, Ca...|      10000|          112|           10|
|   1300172|      The House Lady|     COMMUNITY|Knoxville, Tennes...|      15000|           58|           10|
|    783762|A Story Unwritten...|         MOVIE|    Barcelona, Spain|       7500|         7925|          127|
|    781986|Die deutsch - chi...|     EDUCATION|     Berlin, Germany|      12000|          450|           10|
|    475016|6:38: THE DEATH O...|         MOVIE|Los Angeles, Cali...|     200000|          390|           10|
|    370437|            SLOW WIN|         MOVIE|Toronto, Ontario,...|      10000|          740|           19|
|     53835|The Finger and th...|           ART|        Genoa, Italy|      15000|          150|           10|
|    513792|Intrusion Disconn...|         MOVIE|Louisville, Kentu...|      10000|          195|           10|
|   1188584|Wild West Steel -...|SMALL BUSINESS|Idaho Falls, Idah...|      50000|           30|           10|
+----------+--------------------+--------------+--------------------+-----------+-------------+-------------+


ダラダラと長く書いたのですが、以下のように短いコードで簡単なデータ変換が出来ます

transformer.trim_col("*")
.remove_special_chars("*")
.clear_accents("*")
.lower_case("*")
.drop_col("dummyCol")
.set_col(func)
.delete_row(func)

OutlierDetectorクラスを使って外れ値のチェック

以下例


from pyspark.sql.types import StringType, IntegerType, StructType, StructField
schema = StructType([
        StructField("name", StringType(), True),
        StructField("category", StringType(), True),
        StructField("status", IntegerType(), True)])
name = ['Spirit of the Game', 'FoodQuest', 'Connected', 'Arcadian']
category = ['Food', 'Movie', 'Photography', 'Movie']
status = [1,1,1,9]
df = op.spark.createDataFrame(list(zip(name, category, status)), schema=schema)
transformer = op.DataFrameTransformer(df)
transformer.show()

+------------------+-----------+------+
|              name|   category|status|
+------------------+-----------+------+
|Spirit of the Game|       Food|     1|
|         FoodQuest|      Movie|     1|
|         Connected|Photography|     1|
|          Arcadian|      Movie|     9|
+------------------+-----------+------+

detector = op.OutlierDetector(df,"num")
detector.outliers()

[9] <- 外れ値

まとめ

OptimusはSpark上手軽にデータ品質分析、変換が可能なオープンソースで、
この種類のツールが少ないので、非常に貴重だなと感じました。
サンプル類もたくさん用意されているので入門のハードルも低く感じました。

まだ正式リリースしたばかりなので、動かしてみるとドキュメントとおり行かない部分とかもありましたが、 helpで仕様確認しながらやれば特に困ることもなかったので、これからが楽しみなツールです。

ではまた!

Copyright © astamuse company, ltd. all rights reserved.