Python
Graphviz

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


はじめに

昔は夢中になって見ていたF1でしたが、見なくなってから十数年…

久しぶりにF1に関する記事を読んでみたら、あの頃強かったウィリアムズがテールエンダーと化し、当時はいなかったメルセデスのワークスが強いらしい…

だいたいメルセデスのワークスってどこを買収して参入してきたんだ?ってことが気になって、コンストラクターの変遷を可視化してみようと思い立ちました。


いきなり結果

PythonとGraphvizでこんな感じに可視化できました。

screenshot1.png

(中略)

screenshot2.png

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


やってみたこと


元データ

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


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, 'トールマン')
]),


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


constructors_data.py

constructor_connect = [

(('シャドウ', 1980), ('セオドール', 1981)),
(('マーチ(1981)', 1982), ('RAM', 1983)),
(('エンサイン', 1982), ('セオドール', 1983)),
(('トールマン', 1985), ('ベネトン', 1986)),



実装

このデータをPythonにてimportして、graphvizを使って可視化しました。

ソースはgithubに置いてありますが、ポイントと思った点を書いておきます。

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


グラフの初期化


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つ属性設定しています。


年のラベル


constructors_graph.py

    # 年をラベルとして表示

for year, _ in constructor_ranking:
g.node(str(year), label=str(year), shape='none')

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


コンストラクターの枠


constructors_graph.py

        # cluster_チーム名という形でチームごとの枠を作る

with g.subgraph(name=f'cluster_{name}') as c:
# 枠の名前はチーム名にしておく
c.attr(label=name)

subgraphを使ってコンストラクタごとの枠を作っています。

「label=name」でコンストラクタ名をラベルにしています。


各年を示すノード

※上のsubgraphのwith内のコードです。


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:])」というところで、ノード間を示すタプル列ができてます。


年ごとに位置を揃える


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ファイルには


{rank=same; "ノード1", "ノード2", }

と記載すれば同じ高さに揃えられるのですが、graphvizではその設定ができなさそうなので、「g.body.append」で直接追加しています。


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


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→ホンダ→ブラウン→メルセデス)、フェラーリ、ウィリアムズ、ベネトン(→ルノー→ロータス→ルノー)、ミナルディ(→トロ・ロッソ)、マクラーレン。

フェラーリのような名門が残っている一方で、どちらかというと常時真ん中より後ろ、という位置だったミナルディの流れが今も続いている、というのが面白いなぁ…なんて思いました。