Python でパックドバブルチャートを作成する

Beginner

This tutorial is from open-source community. Access the source code

はじめに

この実験では、Python の Matplotlib を使ってパックドバブルチャートを作成する方法を学びます。パックドバブルチャートは、異なるサイズのバブルでデータを表示するチャートの一種で、バブルのサイズがデータの大きさを表します。このチャートは、データが項目に関連付けられた単一の数値であるスカラーデータを表示するのに便利です。

VM のヒント

VM の起動が完了したら、左上隅をクリックして ノートブック タブに切り替えて、Jupyter Notebook を使って練習します。

Jupyter Notebook の読み込みには数秒かかる場合があります。Jupyter Notebook の制限により、操作の検証は自動化できません。

学習中に問題がある場合は、Labby にお問い合わせください。セッション後にフィードバックを提供してください。すぐに問題を解決いたします。

必要なライブラリをインポートする

パックドバブルチャートを作成するには、matplotlib.pyplotnumpy ライブラリをインポートする必要があります。numpy ライブラリは、配列に対して数学的演算を行うために使用され、バブルサイズの計算に便利です。

import matplotlib.pyplot as plt
import numpy as np

データを定義する

この例で使用するデータは、異なるデスクトップブラウザの市場シェアです。データを、各バブルのブラウザ名、市場シェア、色を含む辞書として定義します。

browser_market_share = {
    'browsers': ['firefox', 'chrome', 'safari', 'edge', 'ie', 'opera'],
    'market_share': [8.61, 69.55, 8.36, 4.12, 2.76, 2.43],
    'color': ['#5A69AF', '#579E65', '#F9C784', '#FC944A', '#F24C00', '#00B825']
}

BubbleChart クラスを定義する

BubbleChart クラスは、パックドバブルチャートを作成するために使用されます。このクラスは、バブルの面積の配列とバブル間隔の値を受け取ります。__init__ メソッドは、バブルの初期位置を設定し、最大ステップ距離を計算します。最大ステップ距離とは、各バブルが 1 回の反復で移動できる距離です。

class BubbleChart:
    def __init__(self, area, bubble_spacing=0):
        """
        Setup for bubble collapse.

        Parameters
        ----------
        area : array-like
            Area of the bubbles.
        bubble_spacing : float, default: 0
            Minimal spacing between bubbles after collapsing.

        Notes
        -----
        If "area" is sorted, the results might look weird.
        """
        area = np.asarray(area)
        r = np.sqrt(area / np.pi)

        self.bubble_spacing = bubble_spacing
        self.bubbles = np.ones((len(area), 4))
        self.bubbles[:, 2] = r
        self.bubbles[:, 3] = area
        self.maxstep = 2 * self.bubbles[:, 2].max() + self.bubble_spacing
        self.step_dist = self.maxstep / 2

        ## calculate initial grid layout for bubbles
        length = np.ceil(np.sqrt(len(self.bubbles)))
        grid = np.arange(length) * self.maxstep
        gx, gy = np.meshgrid(grid, grid)
        self.bubbles[:, 0] = gx.flatten()[:len(self.bubbles)]
        self.bubbles[:, 1] = gy.flatten()[:len(self.bubbles)]

        self.com = self.center_of_mass()

バブルの移動方法を定義する

BubbleChart クラスには、バブルを質量中心に向かって移動させ、他のバブルとの衝突をチェックするメソッドも含まれています。center_of_mass メソッドは、すべてのバブルの質量中心を計算し、center_distance メソッドは、バブルと質量中心との距離を計算します。outline_distance メソッドは、バブルの輪郭と他のバブルの輪郭との距離を計算し、check_collisions メソッドは、新しいバブルの位置が他のバブルと衝突するかどうかをチェックします。

    def center_of_mass(self):
        return np.average(
            self.bubbles[:, :2], axis=0, weights=self.bubbles[:, 3]
        )

    def center_distance(self, bubble, bubbles):
        return np.hypot(bubble[0] - bubbles[:, 0],
                        bubble[1] - bubbles[:, 1])

    def outline_distance(self, bubble, bubbles):
        center_distance = self.center_distance(bubble, bubbles)
        return center_distance - bubble[2] - \
            bubbles[:, 2] - self.bubble_spacing

    def check_collisions(self, bubble, bubbles):
        distance = self.outline_distance(bubble, bubbles)
        return len(distance[distance < 0])

バブルの衝突方法を定義する

BubbleChart クラスには、バブルの衝突をチェックし、衝突したバブルを周りに移動させるメソッドも含まれています。collides_with メソッドは、新しいバブルの位置と他のバブルの位置との間の距離を計算します。距離がゼロ未満の場合、衝突があることを意味し、このメソッドは衝突したバブルのインデックスを返します。collapse メソッドは、バブルを質量中心に向かって移動させ、衝突したバブルの周りを移動させ、plot メソッドはチャート上にバブルを描画します。

    def collides_with(self, bubble, bubbles):
        distance = self.outline_distance(bubble, bubbles)
        idx_min = np.argmin(distance)
        return idx_min if type(idx_min) == np.ndarray else [idx_min]

    def collapse(self, n_iterations=50):
        """
        Move bubbles to the center of mass.

        Parameters
        ----------
        n_iterations : int, default: 50
            Number of moves to perform.
        """
        for _i in range(n_iterations):
            moves = 0
            for i in range(len(self.bubbles)):
                rest_bub = np.delete(self.bubbles, i, 0)
                ## try to move directly towards the center of mass
                ## direction vector from bubble to the center of mass
                dir_vec = self.com - self.bubbles[i, :2]

                ## shorten direction vector to have length of 1
                dir_vec = dir_vec / np.sqrt(dir_vec.dot(dir_vec))

                ## calculate new bubble position
                new_point = self.bubbles[i, :2] + dir_vec * self.step_dist
                new_bubble = np.append(new_point, self.bubbles[i, 2:4])

                ## check whether new bubble collides with other bubbles
                if not self.check_collisions(new_bubble, rest_bub):
                    self.bubbles[i, :] = new_bubble
                    self.com = self.center_of_mass()
                    moves += 1
                else:
                    ## try to move around a bubble that you collide with
                    ## find colliding bubble
                    for colliding in self.collides_with(new_bubble, rest_bub):
                        ## calculate direction vector
                        dir_vec = rest_bub[colliding, :2] - self.bubbles[i, :2]
                        dir_vec = dir_vec / np.sqrt(dir_vec.dot(dir_vec))
                        ## calculate orthogonal vector
                        orth = np.array([dir_vec[1], -dir_vec[0]])
                        ## test which direction to go
                        new_point1 = (self.bubbles[i, :2] + orth *
                                      self.step_dist)
                        new_point2 = (self.bubbles[i, :2] - orth *
                                      self.step_dist)
                        dist1 = self.center_distance(
                            self.com, np.array([new_point1]))
                        dist2 = self.center_distance(
                            self.com, np.array([new_point2]))
                        new_point = new_point1 if dist1 < dist2 else new_point2
                        new_bubble = np.append(new_point, self.bubbles[i, 2:4])
                        if not self.check_collisions(new_bubble, rest_bub):
                            self.bubbles[i, :] = new_bubble
                            self.com = self.center_of_mass()

            if moves / len(self.bubbles) < 0.1:
                self.step_dist = self.step_dist / 2

    def plot(self, ax, labels, colors):
        """
        Draw the bubble plot.

        Parameters
        ----------
        ax : matplotlib.axes.Axes
        labels : list
            Labels of the bubbles.
        colors : list
            Colors of the bubbles.
        """
        for i in range(len(self.bubbles)):
            circ = plt.Circle(
                self.bubbles[i, :2], self.bubbles[i, 2], color=colors[i])
            ax.add_patch(circ)
            ax.text(*self.bubbles[i, :2], labels[i],
                    horizontalalignment='center', verticalalignment='center')

BubbleChart オブジェクトを作成してチャートを描画する

パックドバブルチャートを作成するには、BubbleChart オブジェクトを作成し、collapse メソッドを呼び出してバブルを質量中心に向かって移動させます。その後、matplotlib のグラフを作成し、そこに軸オブジェクトを追加します。最後に、plot メソッドを呼び出してチャート上にバブルを描画します。

bubble_chart = BubbleChart(area=browser_market_share['market_share'],
                           bubble_spacing=0.1)

bubble_chart.collapse()

fig, ax = plt.subplots(subplot_kw=dict(aspect="equal"))
bubble_chart.plot(
    ax, browser_market_share['browsers'], browser_market_share['color'])
ax.axis("off")
ax.relim()
ax.autoscale_view()
ax.set_title('Browser market share')

plt.show()

まとめ

この実験では、Python の Matplotlib を使ってパックドバブルチャートを作成する方法を学びました。データを、各バブルのブラウザ名、市場シェア、色を含む辞書として定義しました。その後、バブルの初期位置を設定し、最大ステップ距離を計算し、バブルを質量中心に向かって移動させ、他のバブルとの衝突をチェックするための BubbleChart クラスを作成しました。最後に、matplotlib を使ってチャートを描画しました。