txqz memo

Jungで相関行列のグラフ化

ファイルからデータを読み込んでグラフを表示する

JUNGはPageRankや中心性の計算やクラスタリングとかにも使えるJavaのグラフ構造ライブラリなのだが、日本語のまとまった解説文書がTECHSCOREくらいにしか見当たらない。でもTECHSCOREの解説を見れば大体使えたりする。使い方が変更されているメソッドや非推奨になったクラスもたまにあるが、JavaDocを見ればだいたい解決する。

ということで、相関行列を読み込んで、有意な関係をグラフ化するものをJUNGを使って作ってみようと思う。そのために、SourceForgeからJUNGのJARファイルをダウンロードし、依存ライブラリであるCommons CollectionCern Colt(行列計算ライブラリ)をビルドパスに含める。

まず相関行列のデータが必要なので、アイスクリーム統計学のデータから作成した。ExcelファイルをダウンロードしてPEARSON関数を20回コピペするだけの簡単なお仕事。終わったら表頭とデータ部分をコピーしてicecream.txtを作成する。1行目は「バニラ」や「ストロベリー」といった表頭変数がタブ区切りで並び、2行目から相関行列の各行ベクトルがタブ区切りで並ぶことになる。それらを読み込んでVertexを作り、閾値以上のVertexにEdgeを張ってJFrameに表示させる。ソースはこんな感じ:

package net.txqz.neta;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.LineNumberReader;

import javax.swing.JFrame;

import edu.uci.ics.jung.graph.Graph;
import edu.uci.ics.jung.graph.Vertex;
import edu.uci.ics.jung.graph.impl.UndirectedSparseEdge;
import edu.uci.ics.jung.graph.impl.UndirectedSparseGraph;
import edu.uci.ics.jung.graph.impl.UndirectedSparseVertex;
import edu.uci.ics.jung.visualization.FRLayout;
import edu.uci.ics.jung.visualization.Layout;
import edu.uci.ics.jung.visualization.PluggableRenderer;
import edu.uci.ics.jung.visualization.VisualizationViewer;

public class RelationGraph {
  public static void main(String[] args) throws NumberFormatException, IOException {
    JFrame window = new JFrame("RelationGraph");
    final Graph graph = new UndirectedSparseGraph();
    double threshold = 0.2;
    // 表頭変数の配列
    String[] names = null;
    // 相関行列
    double[][] correlation = null;
    LineNumberReader in = new LineNumberReader(
        new InputStreamReader(new FileInputStream("D:\\\\data\\\\icecream.txt"), "MS932")
    );
    String line;
    while((line = in.readLine()) != null) {
      // 1行目は表頭変数。ついでに列の数も数えて配列を作成する。
      if(in.getLineNumber() == 1) {
        names = line.split("\\t");
        correlation = new double[names.length][names.length];
      }
      // 2行目以降はn行目のデータを配列の[n-2]番目に入れる。
      else {
        String[] t = line.split("\\t");
        double[] d = new double[names.length];
        for(int i = in.getLineNumber() - 1; i < t.length; i++) {
          d[i] = Double.parseDouble(t[i]);
        }
        correlation[in.getLineNumber() - 2] = d;
      }
    }

    // Vertexの配列を作ってそれぞれに参照を代入
    Vertex[] vertices = new Vertex[names.length];
    for(int i = 0; i < names.length; i++) {
      vertices[i] = graph.addVertex(new UndirectedSparseVertex());
    }

    // 閾値以上の相関係数を持っているVertex同士にEdgeを結ぶ
    for(int i = 0; i < correlation.length - 1; i++) {
      for(int j = i + 1; j < correlation[i].length; j++) {
        double current = correlation[i][j];
        if(current >= threshold || current <= -threshold) {
          graph.addEdge(new UndirectedSparseEdge(vertices[i], vertices[j]));
        }
      }
    }

    Layout layout = new FRLayout(graph);
    PluggableRenderer renderer = new PluggableRenderer();

    VisualizationViewer viewer = new VisualizationViewer(layout, renderer);

    window.add(viewer);
    window.setSize(600, 600);
    window.setLocationRelativeTo(null);
    window.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    window.setVisible(true);
  }
}

図1: まず表示させてみた

ノードの名前を表示させる

丸と線しかないので何がなんだかわからない。どのアイスがどの頂点に対応しているのかがわからないと解釈のしようがない。そのためにStringLabellerを使う。あと、頂点と頂点を結ぶ線に相関係数のデータを含ませるためにEdgeWeightLabellerが使える。該当部分を書き直してみると:

    // Vertexの配列を作ってそれぞれに参照を代入
    Vertex[] vertices = new Vertex[names.length];
    final StringLabeller stringLabeller = StringLabeller.getLabeller(graph);
    final EdgeWeightLabeller weightLabeller = EdgeWeightLabeller.getLabeller(graph);

    for(int i = 0; i < names.length; i++) {
      vertices[i] = graph.addVertex(new UndirectedSparseVertex());
      try {
        stringLabeller.setLabel(vertices[i], names[i]);
      } catch (UniqueLabelException e1) {
        e1.printStackTrace();
      }
    }

    // 閾値以上の相関係数を持っているVertex同士にEdgeを結ぶ
    for(int i = 0; i < correlation.length - 1; i++) {
      for(int j = i + 1; j < correlation[i].length; j++) {
        double current = correlation[i][j];
        if(current >= threshold || current <= -threshold) {
          Edge e = graph.addEdge(new UndirectedSparseEdge(vertices[i], vertices[j]));
          weightLabeller.setNumber(e, current);
        }
      }
    }

そしてPluggableRendererに:

    renderer.setVertexStringer(new VertexStringer() {
      public String getLabel(ArchetypeVertex v) {
        return stringLabeller.getLabel(v);
      }
    });

とすると下図のようにノードに名前が表示されるようになる。グラフがどのように表示されるかは実行のたびに変化するが、トポロジー的には変化していない。TECHSCOREの解説では UserDataContainer を使っているが、 StringLabeller を使うほうが直感的だと思う。執筆時にはなかった機能なのかもしれない。

図2: ノードに名前が表示された

相関がプラスかマイナスかで色を変える

さきほど EdgeWeightLabeller に相関係数を入れたので、これを使って辺に色をつけてみる。こういうのは PluggableRenderer をいじると実現できる。

    renderer.setEdgePaintFunction(new EdgePaintFunction() {
      public Paint getDrawPaint(Edge e) {
        return weightLabeller.getNumber(e).doubleValue() > 0
               ? Color.PINK
               : Color.CYAN;
      }
      public Paint getFillPaint(Edge e) {
        return null;
      }
    });

図3: 色をつけた

相関の強さに応じて線の種類を変える

同様に、EdgeWeightLabellerを使って辺の種類も変えてみる。

たとえば、相関係数0.4未満を点線、0.4以上を太線にするなら:

    renderer.setEdgeStrokeFunction(new EdgeStrokeFunction() {
      public Stroke getStroke(Edge e) {
        return Math.abs(weightLabeller.getNumber(e).doubleValue()) < 0.4
               ? PluggableRenderer.DOTTED
               : new BasicStroke(2);
      }
    });

図4: だいぶ見通しがついてきた

選択されたノードと、それに隣接しているノードの色を変える

頂点をクリックすると、選択された頂点と辺で結ばれた隣の頂点の色と選択された頂点から延びる辺の色が変わるようにしたい。

頂点や辺が選択されたことを検知するにはPickedInfoを使う。 PluggableRendererPickedInfo インタフェースを実装しているが、 PluggableRenderer#isPicked() はDeprecatedなので、 PickedInfo にキャストしてから isPicked メソッドを実行する必要がある。先ほどいじっていた PluggableRenderer#setEdgePaintFunction()PluggableRenderer#setEdgeStrokeFunction() 、あと加えて PluggableRenderer#setVertexPaintFunction() を変更する。非選択ノードをデフォルトの赤から灰色に変えた上で選択されたノードは橙色に、隣接するノードのうち相関係数がプラスなのは赤でマイナスなのは青に、隣接するアークも同様に相関係数によって青か赤に変更し、点線も太くしよう。内部クラスからrendererを呼ぶので、rendererにfinal宣言が必要。

    renderer.setVertexPaintFunction(new VertexPaintFunction() {
      public Paint getDrawPaint(Vertex v) {
        return Color.DARK_GRAY;
      }

      public Paint getFillPaint(Vertex v) {
        if(((PickedInfo)renderer).isPicked(v))
          return Color.ORANGE;
        for(Object n : v.getNeighbors()) {
          if(((PickedInfo)renderer).isPicked((Vertex)n))
            return weightLabeller.getNumber(v.findEdge((Vertex)n)).doubleValue() < 0
                   ? Color.BLUE
                   : Color.RED;
        }
        return Color.LIGHT_GRAY;
      }
    });
    renderer.setEdgePaintFunction(new EdgePaintFunction() {
      public Paint getDrawPaint(Edge e) {
        for(Object v : e.getIncidentVertices()) {
          if(((PickedInfo)renderer).isPicked((Vertex)v)) {
            return weightLabeller.getNumber(e).doubleValue() < 0
                   ? Color.BLUE
                   : Color.RED;
          }
        }
        return weightLabeller.getNumber(e).doubleValue() > 0
               ? Color.PINK
               : Color.CYAN;
      }
      public Paint getFillPaint(Edge e) {
        return null;
      }
    });
    renderer.setEdgeStrokeFunction(new EdgeStrokeFunction() {
      public Stroke getStroke(Edge e) {
        for(Object v : e.getIncidentVertices()) {
          if(((PickedInfo)renderer).isPicked((Vertex)v)) {
            return Math.abs(weightLabeller.getNumber(e).doubleValue()) < 0.4
                   ? new BasicStroke(2, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND, 1.0f, new float[]{1.0f, 5.0f}, 0f)
                   : new BasicStroke(2);
          }
        }
        return Math.abs(weightLabeller.getNumber(e).doubleValue()) < 0.4
               ? PluggableRenderer.DOTTED
               : new BasicStroke(2);
      }
    });

図5: 選択されたノードとその隣接ノードの表現を変えた

PluggableGraphMouse で操作性を向上する

ノードをクリックして自分自身と隣接ノードの色を変えられたのはいいが、左ボタンから指を離すと色が元に戻ってしまう。それが不便なときはPickingGraphMousePluginPluggableGraphMouseに加えるとよい。

    PluggableGraphMouse gm = new PluggableGraphMouse();
    gm.add(new PickingGraphMousePlugin());

    VisualizationViewer viewer = new VisualizationViewer(layout, renderer);
    viewer.setGraphMouse(gm);

その他、 グラフを回転させたい ならRotatingGraphMousePlugin(Shiftを押しながらドラッグするとグラフを回転できる)を、PDFビューワのように ドラッグでグラフ表示領域の移動がしたい ならTranslatingGraphMousePluginを、 ホイール回転で拡大縮小させたい ならScalingGraphMousePluginをそれぞれ PluggableGraphMouse にaddしていくとよい。こういうビジュアルな効果はこだわりだすと果てしないけどやってて面白い。拡大したときとかにスクロールバーがほしければGraphZoomScrollPaneをJFrameオブジェクトにaddすれば表示されるようになる。

図6: ズームとスクロールバー

最終的にソースは以下のようになった:

package net.txqz.neta;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Paint;
import java.awt.Stroke;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.LineNumberReader;

import javax.swing.JFrame;

import edu.uci.ics.jung.graph.ArchetypeVertex;
import edu.uci.ics.jung.graph.Edge;
import edu.uci.ics.jung.graph.Graph;
import edu.uci.ics.jung.graph.Vertex;
import edu.uci.ics.jung.graph.decorators.EdgePaintFunction;
import edu.uci.ics.jung.graph.decorators.EdgeStrokeFunction;
import edu.uci.ics.jung.graph.decorators.EdgeWeightLabeller;
import edu.uci.ics.jung.graph.decorators.StringLabeller;
import edu.uci.ics.jung.graph.decorators.VertexPaintFunction;
import edu.uci.ics.jung.graph.decorators.VertexStringer;
import edu.uci.ics.jung.graph.decorators.StringLabeller.UniqueLabelException;
import edu.uci.ics.jung.graph.impl.UndirectedSparseEdge;
import edu.uci.ics.jung.graph.impl.UndirectedSparseGraph;
import edu.uci.ics.jung.graph.impl.UndirectedSparseVertex;
import edu.uci.ics.jung.visualization.FRLayout;
import edu.uci.ics.jung.visualization.GraphZoomScrollPane;
import edu.uci.ics.jung.visualization.Layout;
import edu.uci.ics.jung.visualization.PickedInfo;
import edu.uci.ics.jung.visualization.PluggableRenderer;
import edu.uci.ics.jung.visualization.ShapePickSupport;
import edu.uci.ics.jung.visualization.VisualizationViewer;
import edu.uci.ics.jung.visualization.control.LayoutScalingControl;
import edu.uci.ics.jung.visualization.control.PickingGraphMousePlugin;
import edu.uci.ics.jung.visualization.control.PluggableGraphMouse;
import edu.uci.ics.jung.visualization.control.RotatingGraphMousePlugin;
import edu.uci.ics.jung.visualization.control.ScalingGraphMousePlugin;
import edu.uci.ics.jung.visualization.control.TranslatingGraphMousePlugin;

public class RelationGraph {
  public static void main(String[] args) throws NumberFormatException, IOException {
    JFrame window = new JFrame("RelationGraph");
    final Graph graph = new UndirectedSparseGraph();
    double threshold = 0.2;
    // 表頭変数の配列
    String[] names = null;
    // 
    double[][] correlation = null;
    LineNumberReader in = new LineNumberReader(
        new InputStreamReader(new FileInputStream("D:\\\\data\\\\icecream.txt"), "MS932")
    );
    String line;
    while((line = in.readLine()) != null) {
      // 1行目は表頭変数。ついでに列の数も数えて配列を作成する。
      if(in.getLineNumber() == 1) {
        names = line.split("\\t");
        correlation = new double[names.length][names.length];
      }
      // 2行目以降はn行目のデータを配列の[n-2]番目に入れる
      else {
        String[] t = line.split("\\t");
        double[] d = new double[names.length];
        for(int i = in.getLineNumber() - 1; i < t.length; i++) {
          d[i] = Double.parseDouble(t[i]);
        }
        correlation[in.getLineNumber() - 2] = d;
      }
    }

    // Vertexの配列を作ってそれぞれに参照を代入
    Vertex[] vertices = new Vertex[names.length];
    final StringLabeller stringLabeller = StringLabeller.getLabeller(graph);
    final EdgeWeightLabeller weightLabeller = EdgeWeightLabeller.getLabeller(graph);

    for(int i = 0; i < names.length; i++) {
      vertices[i] = graph.addVertex(new UndirectedSparseVertex());
      try {
        stringLabeller.setLabel(vertices[i], names[i]);
      } catch (UniqueLabelException e1) {
        e1.printStackTrace();
      }
    }

    // 閾値以上の相関係数を持っているVertex同士にEdgeを結ぶ
    for(int i = 0; i < correlation.length - 1; i++) {
      for(int j = i + 1; j < correlation[i].length; j++) {
        double current = correlation[i][j];
        if(current >= threshold || current <= -threshold) {
          Edge e = graph.addEdge(new UndirectedSparseEdge(vertices[i], vertices[j]));
          weightLabeller.setNumber(e, current);
        }
      }
    }

    Layout layout = new FRLayout(graph);
    final PluggableRenderer renderer = new PluggableRenderer();

    renderer.setVertexStringer(new VertexStringer() {
      public String getLabel(ArchetypeVertex v) {
        return stringLabeller.getLabel(v);
      }
    });

    renderer.setVertexPaintFunction(new VertexPaintFunction() {
      public Paint getDrawPaint(Vertex v) {
        return Color.DARK_GRAY;
      }

      public Paint getFillPaint(Vertex v) {
        if(((PickedInfo)renderer).isPicked(v))
          return Color.ORANGE;
        for(Object n : v.getNeighbors()) {
          if(((PickedInfo)renderer).isPicked((Vertex)n))
            return weightLabeller.getNumber(v.findEdge((Vertex)n)).floatValue() < 0
                   ? Color.BLUE
                   : Color.RED;
        }
        return Color.LIGHT_GRAY;
      }
    });
    renderer.setEdgePaintFunction(new EdgePaintFunction() {
      public Paint getDrawPaint(Edge e) {
        for(Object v : e.getIncidentVertices()) {
          if(((PickedInfo)renderer).isPicked((Vertex)v)) {
            return weightLabeller.getNumber(e).floatValue() < 0
                   ? Color.BLUE
                   : Color.RED;
          }
        }
        return weightLabeller.getNumber(e).doubleValue() > 0
               ? Color.PINK
               : Color.CYAN;
      }
      public Paint getFillPaint(Edge e) {
        return null;
      }
    });
    renderer.setEdgeStrokeFunction(new EdgeStrokeFunction() {
      public Stroke getStroke(Edge e) {
        for(Object v : e.getIncidentVertices()) {
          if(((PickedInfo)renderer).isPicked((Vertex)v)) {
            return Math.abs(weightLabeller.getNumber(e).floatValue()) < 0.4
                   ? new BasicStroke(2, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND, 1.0f, new float[]{1.0f, 5.0f}, 0f)
                   : new BasicStroke(2);
          }
        }
        return Math.abs(weightLabeller.getNumber(e).floatValue()) < 0.4
               ? PluggableRenderer.DOTTED
               : new BasicStroke(2);
      }
    });

    PluggableGraphMouse gm = new PluggableGraphMouse();
    gm.add(new PickingGraphMousePlugin());
    gm.add(new RotatingGraphMousePlugin());
    gm.add(new TranslatingGraphMousePlugin());
    gm.add(new ScalingGraphMousePlugin(new LayoutScalingControl(), 0));

    VisualizationViewer viewer = new VisualizationViewer(layout, renderer);
    viewer.setGraphMouse(gm);
    viewer.setPickSupport(new ShapePickSupport(viewer, viewer, renderer, 2));

    window.add(viewer);
    window.setSize(600, 600);
    window.setLocationRelativeTo(null);
    window.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    window.add(new GraphZoomScrollPane(viewer));
    window.setVisible(true);
  }
}

マジックナンバーが多いとか、そもそもこんなの全部mainメソッドの中でやっていいのかとか、まああるけど簡単なサンプルということで。あと、このサーバでJavaが動けばこの下にAppletとか置けてよりそれっぽい紹介ができるのだけど、動かないので残念。まあ、ヘタにAppletなんか置くとページの読み込みが激重になるだろうし、いいか。