13장 고급 데이터 시각화

13.1 계층적 데이터시각화

계층적 데이터는 다음 예와 같이 부모-자식-손자 등의 형태로 표현되는 데이터를 의미한다.

{ “name”:”A1″,

“children”:[

{ “name”:”B1″,

“children”:[

{ “name”:”C1″, “value”:100 },

{ “name”:”C2″, “value”:300 },

{ “name”:”C3″, “value”:200 }

]

},

{ “name”:”B2″, “value”:200 }

]

}

D3는 계층적 데이터의 시각화를 위하여 여러 가지 레이아웃을 제공한다. 여기서는 tree, cluster, treemap, pack, partition 등의 레이아웃을 살펴본다. 이와 같은 레이아웃은 노드가 있고 그 노드의 값(인구수, 수입 등)을 갖는다. D3는 필요할 경우 사용자가 직접 레이아웃을 만들 수도 있다.

레이아웃이란 자바스크립트 함수로서 위와 같은 데이터를 입력받아 좌표나 크기 등 시각화를 위한 변수를 계산하여 그래픽으로 표현해 준다. 예를 들어 tree 레이아웃은 <그림 13.1>과 같이 계층적 데이터를 노드 구조로 만들고 각 노드에 좌표 (x, y)를 계산한 후 트리 모양으로 표시해 준다.

<그림 13.1> 계층적 데이터의 트리 그래프 생성 과정

D3로 계층적 데이터의 그래픽을 그리기 위해서는 먼저 d3.hierarchy 객체를 만들어야 한다. d3.hierarchy 객체란 계층을 나타내는 데이터 구조로서 부모, 자식 노드를 불러오거나 노드간의 패스(path)을 계산하는 많은 메소드를 갖고 있다. 예를 들면 d3.hierarchy 객체란 다음과 같은 집합 관계의 자바스크립트 객체이다.

var data={

“name”:”A1″,

“children”:[

{ “name”:”B1″,

“children”:[

{ “name”:”C1″, “value”:100 },

{ “name”:”C2″, “value”:300 },

{ “name”:”C3″, “value”:200 }

]

},

{ “name”:”B2″, “value”:200

}

]

}

var root=d3.hierarchy(data)

hierarchy 객체는 많은 유용한 메소드를 가지고 있다. 예를 들면 root.descendants()는 루트 노드의 모든 후손(descendants)을 리턴해 주고 root.links()는 모든 부모-자식 링크 객체를 리턴한다.

13.1.1 트리(Tree) 그래프

트리 그래프(Tree Graph)는 계층적 데이터를 트리 모양으로 노드(node)와 엣지(edge) 형태로 표현한 것이다. D3의 tree 레이아웃은 다음 명령문으로 시작한다.

var treeLayout=d3.tree();

트리의 크기는 다음 예와 같이 .size()를 이용한다.

treeLayout.size([400,200]);

다음은 hierarchy 객체의 뿌리(root)를 treeLayout()으로 전달한다. 그러면 root안의 각각의 노드(node)와 링크(link)에 대한 (x,y) 좌표가 계산된다.

treeLayout(root);

root.descendants()로 모든 노드의 배열을 얻은 후 원 또는 다른 SVG 요소와 연결한다. 다음 예와 같이 노드의 x, y 좌표를 이용하여 원의 위치를 지정한다.

d3.select(“svg g.nodes”)

.selectAll(“circle.node”)

.data(root.descendants())

.enter()

.append(“circle”)

.classed(“node”, true)

.attr(“cx”, function(d) {return d.x;})

.attr(“cy”, function(d) {return d.y;})

.attr(“r”, 4);

root.links()로 모든 링크(link)의 배열을 얻은 후 이 배열을 선(line) 또는 패스(path)와 연결한다. 이 배열은 선이 시작되는 노드(source node)와 종료 노드(target node)에 대한 정보를 갖고 있다. 다음 예와 같이 x, y 좌표를 이용하여 링크 선의 위치를 지정한다.

d3.select(“svg g.links”)

.selectAll(“line.link”)

.data(root.links())

.enter()

.append(“line”)

.classed(‘link’,true)

.attr(‘x1’,function(d){returnd.source.x;})

.attr(‘y1’,function(d){returnd.source.y;})

.attr(‘x2’,function(d){returnd.target.x;})

.attr(‘y2’,function(d){returnd.target.y;});

13.1.2 클러스터(Cluster) 그래프

클러스터 그래프(Cluster Graph)는 트리 그래프와 유사한데 루트 노드를 제외한 자손이 없는 노드( 이를 잎(leaf) 노드라고 함)를 같은 계층에 위치하도록 한 것이다. 데이터 마이닝에 많이 사용하는 계층적 집락분석(hierarchical cluster analysis)의 결과를 표시할 때 많이 이용된다.

D3의 cluster 레이아웃은 다음 명령문으로 시작한다.

var clusterLayout = d3.cluster();

트리의 크기는 size()를 이용한다.

clusterLayout.size([400,200]);

다음은 hierarchy 객체의 뿌리(root)를 clusterLayout()으로 전달한다. 그러면 root안의 각각의 노드(node)와 링크(link)에 대한 (x,y) 좌표가 계산된다.

clusterLayout(root);

 

13.1.3 트리맵(Treemap) 그래프

트리맵 그래프(Treemap Graph)는 밴 스나이더맨(Ben Shneiderman)에 의해 발명된 것으로 계층적 데이터의 각각의 값을 시각적으로 표현해 준다. 예를 들어 대륙별(첫째 단계) 각 나라(둘째 단계)의 인구를 계층적 데이터로 보았을 때  트리맵은 각 나라를 인구에 비례한 사각형으로 표시하며 각 대륙별로 그룹화한다.

D3의 treemap 레이아웃은 다음 명령문으로 시작한다.

var treemapLayout = d3.treemap();

트리맵 레이아웃의 크기는 size()메소드를 이용한다.

treemapLayout.size([400,200]);

트리맵 레이아웃을 적용하기 전에 sum() 메소드를 이용하여 계층적 데이터의 각 자식 노드의 합계를 구하여야 한다.

root.sum(function(d) { return d.value;});

다음은 hierarchy 객체의 뿌리(root)를 treemapLayout()으로 전달한다. 이 레이아웃은 트리맵의 각 직사각형 노드의 속성을 의미하는 x0, x1, y0, y1을 갖는다.

treemapLayout(root);

이와 같은 직사각형 속성을 d3의 rect 요소와 결합한다.

d3.select(‘svg g’)

.selectAll(‘rect’)

.data(root.descendants())

.enter()

.append(‘rect’)

.attr(‘x’,function(d){returnd.x0;})

.attr(‘y’,function(d){returnd.y0;})

.attr(‘width’,function(d){returnd.x1-d.x0;})

.attr(‘height’,function(d){returnd.y1-d.y0;})

각 직사각형의 레이블을 주려면 rect 요소와 텍스트를 g 요소로 그룹화 하여야 한다.

var nodes=d3.select(‘svg g’)

.selectAll(‘g’)

.data(rootNode.descendants())

.enter()

.append(‘g’)

.attr(‘transform’,function(d){return’translate(‘+[d.x0,d.y0]+’)’})

nodes.append(‘rect’)

.attr(‘width’,function(d){returnd.x1-d.x0;})

.attr(‘height’,function(d){returnd.y1-d.y0;})

nodes.append(‘text’)

.attr(‘dx’,4)

.attr(‘dy’,14)

.text(function(d){

returnd.data.name;

})

treemap 레이아웃은 여러 가지 방법으로 모양을 조정할 수 있다.

– 한 노드의 자식 주변의 패딩은 .paddingOuter 사용

– 사촌 관계 노드 사이의 패딩은 .paddingInner 사용

– outer 와 inner 패딩은 동시하려면 .padding 사용

– outer 패딩은 .paddingTop, .paddingBottom, .paddingLeft, .paddingRight로 조정

트리맵의 각 사각형을 구성하는 방법을 타일링(tiling)이라 하는데 D3는 treemapBinary, treemapDice, treemapSlice, treemapSliceDice, treemapSquarify와 같은 여러 가지 메소드를 제공한다. 사용 방법은 .tile() 메소드를 이용한다.

treemapLayout.tile(d3.treemapDice)

treemapBinary    : 균형적인 수평과 수직 분할

treemapDice      : 수평 분할

treemapSlice     : 수직 분할

treemapSliceDice : 교대로 수평과 수직 분할

treemapSquarify  : 직사각형들의 비율을 지정

<그림 13.1.3> 트리맵의 분할 방법

<그림 13.1.3 – 계속> 트리맵의 분할 방법

 

 

13.1.4 팩(Pack) 그래프

팩 그래프(Pack Graph)는 트리맵 그래프와 유사하나 노드의 표현을 사각형 대신 원을 사용한다. 예를 들어 대륙별(첫째 단계) 각 나라(둘째 단계)의 인구를 계층적 데이터로 보았을 때  팩 그래픽은 각 나라를 인구에 비례한 원으로 표시하며 각 대륙별로 그룹화한다.

D3의 pack 레이아웃은 다음 명령문으로 시작한다.

var packLayout=d3.pack();

팩 레이아웃의 크기는 다음 예와 같이 size()메소드를 이용한다.

packLayout.size([400,200]);

트리맵과 마찬가지로 팩 레이아웃을 적용하기 전에 sum() 메소드를 이용하여 계층적 데이터의 각 자식 노드의 합계를 구하여야 한다.

root.sum(function(d) { return d.value;});

다음은 hierarchy 객체의 뿌리(root)를 packLayout()으로 전달한다. 이 레이아웃은 팩 그래프의 각 원 노드의 속성을 의미하는 x, y, r(반지름)을 갖는다.

packLayout(root);

이와 같은 원 노드의 속성을 d3의 circle 요소와 결합한다.

d3.select(‘svg g’)

.selectAll(‘circle’)

.data(root.descendants())

.enter()

.append(‘circle’)

.attr(‘cx’,function(d){returnd.x;})

.attr(‘cy’,function(d){returnd.y;})

.attr(‘r’,function(d){returnd.r;})

각 원의 레이블을 주려면 circle 요소와 텍스트를 g 요소로 그룹화 하여야 한다.

var nodes = d3.select(‘svg g’)

.selectAll(‘g’)

.data(root.descendants())

.enter()

.append(‘g’)

.attr(‘transform’,function(d){return’translate(‘+[d.x,d.y]+’)’})

nodes.append(‘circle’)

.attr(‘r’,function(d){returnd.r;})

nodes.append(‘text’)

.attr(‘dy’,4)

.text(function(d){

returnd.children===undefined ? d.data.name:”;

})

원의 패딩은 .padding() 메소드를 이용한다.

packLayout.padding(10)

13.1.5 파티션(Partition) 그래프

파티션 그래프(Partition Graph)는 트리맵과 유사하게 계층적 데이터를 직사각형 노드로 분할하는(partition) 그래프이다. 각 계층의 직사각형 노드는 자식의 크기에 따라 다시 분할된다.

D3의 partition 레이아웃은 다음 명령문으로 시작한다.

var partitionLayout = d3.partition();

파티션 레이아웃의 크기는 다음 예와 같이 .size()를 이용한다.

partitionLayout.size([400,200]);

트리맵과 마찬가지로 partition 레이아웃을 적용하기 전에 sum() 메소드를 이용하여 계층적 데이터의 각 자식 노드의 합계를 구하여야 한다.

root.sum(function(d) { return d.value;});

다음은 hierarchy 객체의 뿌리(root)를 partitionLayout()으로 전달한다. 이 레이아웃은 파티션 그래프의 각 직사각형 노드의 속성을 의미하는 x0, x1, y0, y1을 갖는다.

partitionLayout(root);

이와 같은 직사각형 노드의 속성을 d3의 rect 요소와 결합한다.

var nodes = d3.select(‘svg g’)

.selectAll(‘rect’)

.data(rootNode.descendants())

.enter()

.append(“g”)

nodes.append(‘rect’)

.attr(‘x’, function(d) { return d.x0; })

.attr(‘y’, function(d) { return d.y0; })

.attr(‘width’, function(d) { return d.x1 – d.x0; })

.attr(‘height’, function(d) { return d.y1 – d.y0; })

각 직사각형의 레이블을 주려면 circle 요소와 텍스트를 g 요소로 그룹화 하여야 한다.

var margin = 10;

nodes.append(“text”)

.attr(‘x’, function(d) { return d.x0 + margin; })

.attr(‘y’, function(d) { return d.y0 + margin; })

.text(function(d) {return d.data.name;})

파티션의 패딩은 .padding() 메소드를 이용한다.

partitionLayout.padding(10)

위의 예제는 위한 직사각형 노드를 위에서 아래로 계층적 데이터를 표시하였다. 다음과 같이 하면 왼쪽에서 오른쪽으로 계층적 데이터를 표시하는 파티션 그래픽을 그릴 수 있다.

파티션 레이아웃의 직사각형 노드대신 원을 사용하면 선버스트(sunburst) 그래프가 된다. x 좌표를 회전 각도로 하고, y좌표를 반지름으로 하여 계층적 데이터를 표시한다. 아래 프로그램에서 path의 스타일은 path {fill:indianred; opacity:0.3; stroke:white; } 이다.

13.2 네트워크(Network) 그래프

네트워크 그래프(Network Graph)는 노드(node)와 엣지(edge)로서 데이터의 관련성을 표시하는 그래프이다. 노드는 대개 원이나 사각형으로 표시하고 엣지는 선으로 표시한다. 13.1절의 계층적 데이터를 표현한 트리나 클러스터 그래픽은 일종의 네트워크 그래프이다. 요사이 트위터와 페이스북 등 소설 네트워크가 많이 이용됨에 따리 네트워크 분석 및 시각화가 많이 필요하게 되었다. 네트워크 분석은 네트워크 그래프에서 주목할 만한 패턴을 찾는 것을 의미한다.

D3는 복잡한 네트워크도 그릴 수 있는 다양한 명령어를 가지고 있다. 하지만 이 책에서는 기본적인 네트워크 데이터를 이용한 간단한 네트워크 그래프만 소개한다. 전문적인 네트워크 그래픽을 위해서는 많은 노력이 필요한데 관련 문헌을 참고하기 바란다.

기본적으로 많이 이용되는 네트워크 데이터 형태는 노드와 링크에 대한 정보이다. 노드는 이름과 그룹이 표시되어 있고, 링크는 시작 노드(source node)와 목표 노드(target node) 그리고 가중값(weight)이 무엇인지 표시되어 있다. 이때 “source”:0 은 첫째 노드 “node1″을 의미하고 “source”:2 은 셋째 노드 “node3” 등을 의미한다. 가중값은 대개 선의 굵기를 표시하는데 이용된다.

var dataSet = {

“nodes”:[

{“name”:”node1″},

{“name”:”node2″},

{“name”:”node3″},

{“name”:”node4″},

],

“links”:[

{“source”:2,”target”:1,”weight”:1},

{“source”:0,”target”:2,”weight”:2},

{“source”:3,”target”:1,”weight”:3}

]

}

네트워크 그래픽을 간단히 설명하면 위의 노드(nodes) 데이터를 원 또는 사각형으로 표시하고, 링크(links) 정보를 이용하여 노드와 노드를 직선으로 연결(엣지(edge)라고 함)하는 것이다. SVG 그래프 캔버스의 어디에 노드를 그리며 노드 사이의 관계를 표시하는 엣지의 가중, 방향성 등을 어떻게 그리는 가에 따라 많은 알고리즘이 있을 수 있다.

D3의 네트워크 그래픽은 힘-방향(force-directed) 레이아웃을 사용한다. 힘-방향이란 물리학에 기원하여 네트워크를 구성한다는 의미로서 한 공간의 입자(노드)들은 서로 밀어내는 힘이 작용하고(D3에서 charge() 메소드), 공간에 배치된 입자들은 중앙에서  끌어당기는 힘이 작용하며(D3에서 gravity() 메소드), 연결된 입자들끼리는 서로가 끌어당기는 힘이 작용한다(D3에서 linkDistance()와 linkStrength() 메소드)는 것이다. 이와 같은 물리학의 힘의 원리로 D3에서는 노드와 엣지가 공간에 적절히 배치되도록 네트워크를 구성한다.

D3는 force 레이아웃에 많은 속성을 지정할 수 있게 하는데 자세한 것은 관련 문서를 참고하기 바란다. 여기서는 간단한 네트워크 그래프 예를 통해 기본적인 속성을 살펴본다.

13.3 워드 클라우드

워드 클라우드(Word Cloud)는 텍스트 데이터에 포함된 단어들의 출현 빈도를 이용하여 많이 나타나는 중요 단어들을 2차원 공간에 시각적으로 표현한다. 이때 단어 출현 빈도를 이용하여 폰트의 크기, 폰트 형태, 회전, 색 등으로 단어를 적절히 표현한다.

워드 클라우드를 위한 중요 단어의 추출은 각 언어의 특성에 따라 어간의 추출, 어미의 제거 등 고려하여야 할 요소가 많기 때문에 해당 언어에 대한 깊이 있는 이해와 문화, 습관에 대한 이해가 필요하다.

워드 클라우드는 데이터베이스의 각 문서별 분석은 아니고 전체 문서의 중요 단어를 단어 구름 형태로 보여주어 데이터 전체가 어떠한 것인지 쉽게 살펴볼 수 있어서 최고 경영자의 의사결정에 도움을 줄 수 있다. 예를 들어 트위터에서 특정 상품의 언급 내용이나, 고객들이 주고받은 문장을 분석하면 한 상품에 대한 선호 경향을 파악할 수 있어 평판에 대한 관리나 마케팅, 경쟁 상품에 대한 전략을 세울 수 있다.

문서들의 집합에서 출현하는 단어의 빈도수를 조사하는 것은 이 책의 범위를 넘는다. 여기서는 다음 예와 같이 단어의 빈도수가 조사된 결과를 이용하여 워드 클라우드를 그려 보자.

13.4 지리정보 그래프

지리정보 그래프(Geographic Information Graph)는 지도와, 지역에 관련된 데이터를 연결하여 분석하기 쉽도록 시각화하는 것이다. 컴퓨터 모니터에 지도를 그려야 하는 10장에서 구글 지도 그리는 방법은 살펴보았다. 하지만 지리정보 그래픽은 구글 지도와 같은 세밀한 지도가 필요하지는 않고 행정구역으로 나누어진 지도가 많이 이용된다.

행정지도를 그리는데 많이 이용되는 방법은 한 지점의 경도(longitude)와 위도(latitude)를 캔버스의 (x,y) 좌표로 이용한다. 우리나라는 경도 E125~131, 위도 N33~39 범주에서 좌표를 추출하게 된다. 우리나라 행정지도를 그리려면 행정구역을 따라 굴곡이 있는 지점들의 (경도, 위도) 좌표를 추출한 후 이들 지점을 직선으로 연결한다. 이와 같이 여러 지점의 좌표를 직선으로 연결하여 생기는 다각형을 폴리곤(polygon)이라 부른다. 우리나라는 섬이 많아 전체 지도를 그리려면 여러 개의 폴리곤이 필요하다. 아주 작은 섬은 생략되기도 한다. 많은 지점의 좌표를 이용하면 세밀한 지도가 되고, 적은 수의 좌표를 이용하면 대략의 윤곽만 나타난다. 하지만 우리가 사용하는 컴퓨터 모니터의 해상도가 제약이 있어 아주 많은 좌표의 폴리곤은 불필요하다.

우리나라의 시/군/구 등의 지도를 그리기 위해서는 폴리곤 데이터가 필요한데 여러 가지 형식이 있다. ESRI라는 곳에서 개발된 벡터 데이터 형식의 SHP 파일(Shape file)이 많이 사용된다. 다음 사이트에서 다운로드 받을 수 있다.

공간정보시스템 연구소, http://www.gisdeveloper.co.kr

SHP 파일은 dbf, shp, shx, prj 등 네 가지의 부속 파일이 있는데 직접 다루기는 힘들고 지도를 표현하기 위해서는 GeoJSON이라는 지리 정보 표시를 위한 표준 Json포맷으로 변환해야 한다. GeoJSON의 포맷은 다양한 지리 데이터 구조를 인코딩하고 표현하기 위한 형식으로 RFC 표준(RFC 7946)으로 정해져 있다.

변환에 앞서 SHP에 표현되는 벡터 정보는 폴리곤들이 꽤 디테일하게 그려져 있기 때문에 사이즈가 크기도 하고 좀더 단순하게 편집할 필요가 있다. 이것도 여러 가지 변환 도구가 있는데 Mapshaper라는 소프트웨어가 많이 사용된다. 자바스크립트로 만들어져 있고 node.js라는 라이브러리를 설치하여 사용한다.

.SHP 파일을 GeoJSON으로 변환해주는 소프트웨어(예: ogr2ogr)도 최근 계속 개발되고 있다. 이러한 일련의 변환 과정의 자세한 설명은 관련 웹사이트를 참조하기 바란다. 이 책에서는 최종 변환된 json 형식의 시도 행정지도 파일 KOR_adm1.json을 이용한다. 이 json 데이터는 폴리곤의 좌표 데이터이다.

{“type”:”Topology”,”arcs”:[[[661833,384586],[0,-56],[46,0],[0,-172],[-46,0],[0,-57],[-189,0],[0,-114],[-46,0],[0,-58],[-49,0],[0,-57],[-47,0],[0,-57],[-46,0],[0,-57],[-142,0],[0,-57],[-93,0],[0,-57],[-142,0],[0,-57],[-186,0],[0,57],[-49,0],[0,171],[49,0],[0,57],[46,0],[0,172],[47,0],[0,57],[142,0],[0,171],[46,0],[0,58],[47,0],[0,56],[46,0],[0,-114],[47,0],[0,-57],[284,0],[0,57],[46,0],[0,114],[189,0]],

[[658448,394757],[0,-57],[47,0],[0,-58],[93,0],[0,-56],[49,0],[0,-115],[46,0],[0,-114],…

지도와 여러 가지 지리적 정보를 결합한 소프트웨어를 지리 정보 시스템(Geographic Information System, GIS)이라 부르고 이와 관련된 전문적인 소프트웨어가 많이 있다. D3를 이용해서도 기본적인 수준의 GIS는 구축할 수 있는데 이 책의 범위를 넘어서므로 관련 책을 참조하기 바란다.

다음은 우리나라 시도별 행정 지도 파일 KOR_adm1.json에 인구 데이터를 결합하여 시각화한 예이다.