Python+GraphvizでF1コンストラクターの変遷を可視化してみた

# はじめに
昔は夢中になって見ていたF1でしたが、見なくなってから十数年…
久しぶりにF1に関する記事を読んでみたら、あの頃強かったウィリアムズがテールエンダーと化し、当時はいなかったメルセデスのワークスが強いらしい…
だいたいメルセデスのワークスってどこを買収して参入してきたんだ?ってことが気になって、コンストラクターの変遷を可視化してみようと思い立ちました。

# いきなり結果
PythonとGraphvizで[こんな感じ](https://masaminh.github.io/constructors_graph/output/constructors.gv.pdf)に可視化できました。
![screenshot1.png](https://qiita-image-store.s3.amazonaws.com/0/83499/808591e2-5a91-b31b-aa43-bab2e3ea8e14.png)
(中略)
![screenshot2.png](https://qiita-image-store.s3.amazonaws.com/0/83499/903cf122-eeb0-2fa8-ec35-f55453b570d3.png)

えっ、メルセデスってティレルの流れなの!?

# やってみたこと
## 元データ
元データは手作業で作成。Pythonにてimportして使うことを想定。年ごとのコンストラクターズランキングとコンストラクター名がわかるような形式にしました。

```python:constructors_data.py
constructor_ranking = [
    (1980, [
        (1, 'ウィリアムズ'), (2, 'リジェ'), (3, 'ブラバム'), (4, 'ルノー(1977)'),
        (5, 'ロータス(1958)'), (6, 'ティレル'), (7, 'アロウズ(1978)'),
        (8, 'フィッティパルディ'), (9, 'マクラーレン'), (10, 'フェラーリ'),
        (11, 'アルファロメオ(1979)'), (0, 'ATS'), (0, 'エンサイン'), (0, 'オゼッラ'),
        (0, 'シャドウ')
    ]),
    (1981, [
        (1, 'ウィリアムズ'), (2, 'ブラバム'), (3, 'ルノー(1977)'), (4, 'リジェ'),
        (5, 'フェラーリ'), (6, 'マクラーレン'), (7, 'ロータス(1958)'),
        (8, 'アロウズ(1978)'), (9, 'アルファロメオ(1979)'), (10, 'ティレル'),
        (11, 'エンサイン'), (12, 'セオドール'), (13, 'ATS'), (14, 'マーチ(1981)'),
        (0, 'フィッティパルディ'), (0, 'オゼッラ'), (0, 'トールマン')
    ]),
    …
```

あと、コンストラクターのつながりを表現するために、以下のようなリストも用意しました。

```python:constructors_data.py
constructor_connect = [
    (('シャドウ', 1980), ('セオドール', 1981)),
    (('マーチ(1981)', 1982), ('RAM', 1983)),
    (('エンサイン', 1982), ('セオドール', 1983)),
    (('トールマン', 1985), ('ベネトン', 1986)),
    …
```

## 実装
このデータをPythonにてimportして、[graphviz](https://pypi.org/project/graphviz/)を使って可視化しました。
ソースは[github](https://github.com/masaminh/constructors_graph)に置いてありますが、ポイントと思った点を書いておきます。

※ソース上「コンストラクター」「チーム」がごっちゃになっていますが、ご了承ください

### グラフの初期化
```python:constructors_graph.py
    g = Digraph('G', filename='constructors.gv')

    # クラスタ内のノードに対してランク付けさせるのに必要
    g.attr('graph', newrank='true')

    # クラスタ間でエッジ接続させるのに必要
    g.attr('graph', compound='true')

    # ノードのデフォルトはbox
    g.attr('node', shape='box')
```

今回はコンストラクターごとにクラスタを作ってその中に各年のコンストラクターランキングをノードで表現するため、クラスタ内のノードを横並びに整列させる必要性、ノード間ではなくクラスタ間で矢印を接続したい、順位のノードは矩形で表現したい、ということで、3つ属性設定しています。

### 年のラベル
```python:constructors_graph.py
    # 年をラベルとして表示
    for year, _ in constructor_ranking:
        g.node(str(year), label=str(year), shape='none')
```

年のラベルとして表現したいのですが、そのままだと矩形で囲われて出力されてしまうため、shape='none'を指定して、ラベル文字列のみ出力するようにしました。

### コンストラクターの枠
```python:constructors_graph.py
        # cluster_チーム名という形でチームごとの枠を作る
        with g.subgraph(name=f'cluster_{name}') as c:
            # 枠の名前はチーム名にしておく
            c.attr(label=name)
```

subgraphを使ってコンストラクタごとの枠を作っています。
「label=name」でコンストラクタ名をラベルにしています。

### 各年を示すノード
※上のsubgraphのwith内のコードです。

```python:constructors_graph.py
            node_names = []

            # 年ごとのノードを作る
            for year, rank in rankings:
                node_name = f'{name}_{year}'
                c.node(node_name, label=f'{rank}位' if rank > 0 else '-')
                node_names.append(node_name)

            # 1つずらすことでノード間のエッジを作る
            c.edges(zip(node_names[:-1], node_names[1:]))
```
「コンストラクタ名\_年」というノードを作成し、ラベルを順位にしています。
node_namesは、年次順に['ウィリアムズ\_1980', 'ウィリアムズ\_1981', 'ウィリアムズ\_1982',…]というリストになっているので、「zip(node\_names[:-1], node\_names[1:])」というところで、ノード間を示すタプル列ができてます。

### 年ごとに位置を揃える

```python:constructors_graph.py
    for year, rankings in constructor_ranking:
        nodes = ', '.join([f'"{name}_{year}"' for _, name in rankings])
        g.body.append('{rank=same; ' + f'"{year}", {nodes}' + '}')
```

Graphvizで処理させるDOTファイルには

```dot

{rank=same; "ノード1", "ノード2", …}
```
と記載すれば同じ高さに揃えられるのですが、[graphviz](https://pypi.org/project/graphviz/)ではその設定ができなさそうなので、「g.body.append」で直接追加しています。

### コンストラクタを示すクラスタ間をつなぐ矢印

```python:constructors_graph.py
    for tail, head in constructor_connect:
        g.edge(f'{tail[0]}_{tail[1]}', f'{head[0]}_{head[1]}',
               lhead=f'cluster_{head[0]}', ltail=f'cluster_{tail[0]}')
```

コンストラクタの名前が切り替わるタイミングでの矢印はノード間ではなくて、クラスタ間で結びたいので、「lhead」「ltail」でクラスタ名を指定しています。

# 最後に

とりあえず1980年から2017年までの変遷を見られるようにしてみました。
1980年からにしたのは、それ以前だとプライベーターが多い関係で、繋がりを追うのがしんどくなってきたため。

実際に可視化して、私が見始めた1987年から何らかの形でつながっているチームは…と見てみると、ティレル(→BAR→ホンダ→ブラウン→メルセデス)、フェラーリ、ウィリアムズ、ベネトン(→ルノー→ロータス→ルノー)、ミナルディ(→トロ・ロッソ)、マクラーレン。
フェラーリのような名門が残っている一方で、どちらかというと常時真ん中より後ろ、という位置だったミナルディの流れが今も続いている、というのが面白いなぁ…なんて思いました。