- Legend(범례)는 데이터의 의미 파악을 도와주는 도구입니다.
- 그러나 그림이 여럿 있을 때 각각 붙은 Legend는 방해가 되기도 합니다.
- Legend를 한데 모아 그리는 방법을 알아봅니다.
1. Sample Data
- 먼저 필요한 라이브러리들을 불러오고,
1
2
3
4
5
6
7
8%matplotlib inline
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
sns.set_style("whitegrid")
sns.set_context("talk")
- 우리의 펭귄을 소환합니다.
1
2df_p = sns.load_dataset("penguins")
df_p.head()
2. 기본 그림
legend를 붙일 그림을 먼저 그립니다.
seaborn의
regplot
을 사용해서 부리 길이, 폭, 날개 길이를 그립니다.scatter_kws와 line_kws로 시각화 요소들의 색상, 크기 등을 설정합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13fig, axs = plt.subplots(ncols=3, figsize=(10, 4),
sharex=True, constrained_layout=True)
sns.regplot(x="body_mass_g", y="bill_length_mm", data=df_p,
ax=axs[0], label="Bill Length", scatter_kws={"s":10, "color":"C1"}, line_kws={"color":"k"})
sns.regplot(x="body_mass_g", y="bill_depth_mm", data=df_p,
ax=axs[1], label="Bill Depth", scatter_kws={"s":10, "color":"C2"}, line_kws={"color":"k"})
sns.regplot(x="body_mass_g", y="flipper_length_mm", data=df_p,
ax=axs[2], label="Flipper Length", scatter_kws={"s":10, "color":"C3"}, line_kws={"color":"k"})
for ax in axs:
ax.set_xlabel("Body Mass")
ax.set_ylabel("")sns.regplot()
함수가 데이터 수만큼 반복되고 있습니다.데이터 수가 10개라면 코드가 그만큼 더 길어질 것입니다.
for loop과 zip을 사용해서 효율적으로 바꿉니다. 조금 짧아지고 유지보수가 편해집니다.
1
2
3
4
5
6
7
8
9fig, axs = plt.subplots(ncols=3, figsize=(10, 4),
sharex=True, constrained_layout=True)
for ax, c, y in zip(axs, ["C1", "C2", "C3"], ["bill_length_mm", "bill_depth_mm", "flipper_length_mm"]):
label = " ".join([w[0].upper()+w[1:] for w in y.split("_")[:-1]])
sns.regplot(x="body_mass_g", y=y, data=df_p,
ax=ax, label=label, scatter_kws={"s":10, "color":c}, line_kws={"color":"k"})
ax.set_xlabel("Body Mass")
ax.set_ylabel("")ax.set_ylabel("")
로 ylabel을 지웠습니다.y 인자 이름을 축 레이블 대신 legend 형태로 표현하기 위해서입니다.
3. Legend 하나씩
3.1. Axes별 Legend
- 가장 기본적인 형태입니다.
- Axes 하나마다
ax.legend()
를 실행합니다. - markerscale은 데이터를 의미하는 마커를 3배 크게 그리라는 의미입니다.
- 잘 보이게 하고 데이터와 혼동되지 않게 하려는 의도입니다.
1
2
3
4
5
6
7
8
9
10
11fig, axs = plt.subplots(ncols=3, figsize=(10, 4),
sharex=True, constrained_layout=True)
for ax, c, y in zip(axs, ["C1", "C2", "C3"], ["bill_length_mm", "bill_depth_mm", "flipper_length_mm"]):
label = " ".join([w[0].upper()+w[1:] for w in y.split("_")[:-1]])
sns.regplot(x="body_mass_g", y=y, data=df_p,
ax=ax, label=label, scatter_kws={"s":10, "color":c}, line_kws={"color":"k"})
ax.set_xlabel("Body Mass")
ax.set_ylabel("")
ax.legend(markerscale=3) # Axes별 Legend
3.2. Axes 공간 전체 사용
- Axes마다 붙긴 했는데 깔끔하지 않습니다. 좀 지저분합니다.
- Axes마다 Legend가 귀퉁이에 쭈그리고 있어서 그런가 싶습니다.
mode="extend"
로 전체 공간을 다 사용하도록 합니다.1
2
3
4
5
6
7
8
9
10
11fig, axs = plt.subplots(ncols=3, figsize=(10, 4),
sharex=True, constrained_layout=True)
for ax, c, y in zip(axs, ["C1", "C2", "C3"], ["bill_length_mm", "bill_depth_mm", "flipper_length_mm"]):
label = " ".join([w[0].upper()+w[1:] for w in y.split("_")[:-1]])
sns.regplot(x="body_mass_g", y=y, data=df_p,
ax=ax, label=label, scatter_kws={"s":10, "color":c}, line_kws={"color":"k"})
ax.set_xlabel("Body Mass")
ax.set_ylabel("")
ax.legend(mode="expand", markerscale=3) # Axes별 Legend, 넓게
3.3. Axes 위에 Legend
- scatter plot은 점 하나하나가 데이터입니다.
- 시각화 요소들에 의해 가려지면 그만큼 데이터 전달력이 손실됩니다.
- Axes 위로 Legend를 올려서 데이터를 잘 보이게 합니다.
1
2
3
4
5
6
7
8
9
10
11
12fig, axs = plt.subplots(ncols=3, figsize=(10, 4),
sharex=True, constrained_layout=True)
for ax, c, y in zip(axs, ["C1", "C2", "C3"], ["bill_length_mm", "bill_depth_mm", "flipper_length_mm"]):
label = " ".join([w[0].upper()+w[1:] for w in y.split("_")[:-1]])
sns.regplot(x="body_mass_g", y=y, data=df_p,
ax=ax, label=label, scatter_kws={"s":10, "color":c}, line_kws={"color":"k"})
ax.set_xlabel("Body Mass")
ax.set_ylabel("")
# Axes별 Legend, 위치 지정
ax.legend(loc="lower center", bbox_to_anchor=[0.5, 1.03], markerscale=3)
- 그런데 여기서 전체 범위를 사용하겠다고
mode="extend"
를 사용하면 오류가 납니다.1
ax.legend(loc="lower center", bbox_to_anchor=[0, 1.03], mode="expand", markerscale=3)
- 매개변수에서 bbox_to_anchor를 제거하고 사용하면 잘 됩니다.
1
ax.legend(loc=[0, 1.03], mode="expand", borderaxespad=0, markerscale=3)
4. Legend 모아 붙이기
- Axes별로 Legend를 출력하지 않고 한데 모으면 더 깔끔합니다.
- 이를 가능하게 하려면 handle과 label이라는 개념을 파악할 필요가 있습니다.
- legend는 의미가 담긴 label과 label이 지칭하는 대상이 있습니다. 이 대상이 handle입니다.
ax.get_legend_handles_labels
명령으로 확인하고 가져올 수 있습니다.- 위 그림의 첫번째 Axes에 담긴 handle과 label은 이렇습니다.
1
2
3handle, label = axs[0].get_legend_handles_labels()
print(f"handle= {handle}")
print(f"label = {label}")- 실행 결과: 시각화를 하나밖에 안했으므로 handle과 label이 하나씩입니다.
1
2handle= [<matplotlib.collections.PathCollection object at 0x7f95fa482e90>]
label = ['Bill Length']
- 실행 결과: 시각화를 하나밖에 안했으므로 handle과 label이 하나씩입니다.
4.1. Axes에 붙이기
- 빈 list를 만들고, 그림을 그릴 때마다 handle과 label을 가져와 모읍니다.
- 그림을 모두 다 그린 후, 맨 마지막 Axes 오른쪽에 붙입니다.
- 추가 공간이 필요하니 그림 가로 폭을 10에서 14로 넓혀줍니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20fig, axs = plt.subplots(ncols=3, figsize=(14, 4),
sharex=True, constrained_layout=True)
handles = []
labels = []
for ax, c, y in zip(axs, ["C1", "C2", "C3"], ["bill_length_mm", "bill_depth_mm", "flipper_length_mm"]):
label = " ".join([w[0].upper()+w[1:] for w in y.split("_")[:-1]])
sns.regplot(x="body_mass_g", y=y, data=df_p,
ax=ax, label=label, scatter_kws={"s":10, "color":c}, line_kws={"color":"k"})
ax.set_xlabel("Body Mass")
ax.set_ylabel("")
# handles, labels 모으기
handle, label = ax.get_legend_handles_labels()
handles.append(*handle)
labels.append(*label)
# 맨 오른쪽 Axes에 붙이기
axs[-1].legend(handles=handles, labels=labels, markerscale=3,
loc="upper left", bbox_to_anchor=[1.1, 1])
4.2. Figure에 붙이기
- 특정 Axes에 속하지 않도록 전체 그림이 담긴 Figure에 붙일 수 있습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19fig, axs = plt.subplots(ncols=3, figsize=(14, 4),
sharex=True, constrained_layout=True)
handles = []
labels = []
for ax, c, y in zip(axs, ["C1", "C2", "C3"], ["bill_length_mm", "bill_depth_mm", "flipper_length_mm"]):
label = " ".join([w[0].upper()+w[1:] for w in y.split("_")[:-1]])
sns.regplot(x="body_mass_g", y=y, data=df_p,
ax=ax, label=label, scatter_kws={"s":10, "color":c}, line_kws={"color":"k"})
ax.set_xlabel("Body Mass")
ax.set_ylabel("")
# handles, labels 모으기
handle, label = ax.get_legend_handles_labels()
handles.append(*handle)
labels.append(*label)
# Figure에 붙이기
fig.legend(handles=handles, labels=labels, markerscale=3)
4.2. Figure의 Axes 옆자리에 붙이기
- Axes를 그대로 놔두고 붙였더니 맨 우측 Axes에 겹쳐 그려졌습니다.
- Axes 옆에 놓기 위해 legend의 위치를 섬세하게 지정합니다.
- loc와 bbox_to_anchor 매개변수는 이런 역할을 합니다.
- loc만 단독으로 사용하면 붙이는 대상에 따라 Figure나 Axes의 지정된 위치에 놓입니다
- loc와 bbox_to_anchor를 함께 사용하면 loc는 legend의 지점이 되고 bbox_to_anchor는 legend가 놓일 위치가 됩니다.
- bbox_to_ancher에 매개변수가 둘 들어가면 위치만, 넷 들어가면 위치와 가로세로 크기입니다.
맨 우측 Axes 오른쪽 상단에 붙도록 값을 지정합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20fig, axs = plt.subplots(ncols=3, figsize=(14, 4),
sharex=True, constrained_layout=True)
handles = []
labels = []
for ax, c, y in zip(axs, ["C1", "C2", "C3"], ["bill_length_mm", "bill_depth_mm", "flipper_length_mm"]):
label = " ".join([w[0].upper()+w[1:] for w in y.split("_")[:-1]])
sns.regplot(x="body_mass_g", y=y, data=df_p,
ax=ax, label=label, scatter_kws={"s":10, "color":c}, line_kws={"color":"k"})
ax.set_xlabel("Body Mass")
ax.set_ylabel("")
# handles, labels 모으기
handle, label = ax.get_legend_handles_labels()
handles.append(*handle)
labels.append(*label)
# figure 옆에 legend 붙이기: 화면엔 정상, 파일은 실패
fig.legend(handles=handles, labels=labels, markerscale=3,
loc="upper left", bbox_to_anchor=[1, 1])화면에는 정상으로 나오지만 파일을 저장하면 그렇지 않습니다.
legend가 전혀 보이지 않습니다.
bbox_to_anchor에서 지정한 x 위치가 Figure의 우측 한계선(1)을 넘었기 때문입니다.
- 파일 출력을 하려면 legend 전체가 Figure 범위 안에 들어와야 합니다.
- 그러려면 Axes를 좌측으로 압축시킬 필요가 있습니다.
fig.tight_layout()
에 rect 매개변수를 넣으면 됩니다.- 충돌 방지를 위해 비슷한 기능을 하는 constrained_layout은 figure 생성 명령에서 삭제합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21fig, axs = plt.subplots(ncols=3, figsize=(14, 4), sharex=True)
handles = []
labels = []
for ax, c, y in zip(axs, ["C1", "C2", "C3"], ["bill_length_mm", "bill_depth_mm", "flipper_length_mm"]):
label = " ".join([w[0].upper()+w[1:] for w in y.split("_")[:-1]])
sns.regplot(x="body_mass_g", y=y, data=df_p,
ax=ax, label=label, scatter_kws={"s":10, "color":c}, line_kws={"color":"k"})
ax.set_xlabel("Body Mass")
ax.set_ylabel("")
# handles, labels 모으기
handle, label = ax.get_legend_handles_labels()
handles.append(*handle)
labels.append(*label)
# Figure 옆에 legend 붙이기. x 좌표 = 0.8
fig.legend(handles=handles, labels=labels, markerscale=3,
loc="upper left", bbox_to_anchor=[0.8, 0.95])
# Axes들 0.8 안쪽으로 압축
fig.tight_layout(rect=[0,0,0.8,1])
4.3. Figure의 Axes 위에 붙이기
- 같은 요령으로 legend를 Axes 위에 모아서 붙일 수 있습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21fig, axs = plt.subplots(ncols=3, figsize=(14, 4), sharex=True)
handles = []
labels = []
for ax, c, y in zip(axs, ["C1", "C2", "C3"], ["bill_length_mm", "bill_depth_mm", "flipper_length_mm"]):
label = " ".join([w[0].upper()+w[1:] for w in y.split("_")[:-1]])
sns.regplot(x="body_mass_g", y=y, data=df_p,
ax=ax, label=label, scatter_kws={"s":10, "color":c}, line_kws={"color":"k"})
ax.set_xlabel("Body Mass")
ax.set_ylabel("")
# handles, labels 모으기
handle, label = ax.get_legend_handles_labels()
handles.append(*handle)
labels.append(*label)
# Figure 위에 legend 붙이기. y 좌표 = 0.9
fig.legend(handles=handles, labels=labels, markerscale=3, ncol=3,
loc="upper left", bbox_to_anchor=[0.045, 0.9, 1, 0.1])
# Axes들 y = 0.9 안쪽으로 압축
fig.tight_layout(rect=[0,0,1,0.9])
5. 정리
- legend는 여러 데이터를 명확히 구분해주는, 반드시 필요한 요소입니다.
- 그러나 Axes가 많아지고 데이터 인자가 많아질수록 혼돈의 원인이 되기도 합니다.
- 적절한 위치에 적절한 형식으로 배치해서 인지능력 향상에 도움이 되면 좋겠습니다.