improvement- COVID antibody holder

  • 최근 무작위 조사 결과 95%의 사람들에게서 코로나19 항체가 발견되었다고 합니다.
  • 한 뉴스에서 이 기사가 보도되었는데 시각화가 적절치 못했습니다.
  • 이를 나름대로 바로잡아 새로 그려봅니다.

채널A: 무작위 조사했더니…국민 100명 중 95명은 코로나 항체 보유

1. 언론 보도

2022.06.14. 채널A 보도 화면

  • 2022년 6월 14일, 1612명을 대상으로 코로나19 항체 보유를 조사한 결과가 보도되었습니다.

  • 전체의 95%에서 항체가 발견되었고 15.7%는 자연 감염 경력이 있다고 합니다.


  • 그런데 문제가 있습니다.

  • 자연 감염 15.7%항체 보유 94.9%가 나란히 놓이는 바람에 별개의 데이터로 보입니다.

  • 심지어 색상을 다르게 사용하는 바람에 정말 다른 종류의 데이터로 느껴집니다.

2. 다시 그리기

강의 활용 코드

  • 동명대학교에서의 강의를 계기로 새로 그려보기로 했습니다.
  • 사실 당일 새벽에 코드를 급하게 작성했기 때문에 강의를 하다가도 아쉬운 부분이 느껴졌고,
  • 한 단계 다시 정리를 하기로 했습니다.
  • Google Colab 코드를 다시 정리합니다.

2.1. 환경 설정

  • Colab에 기본으로 설정된 폰트가 좀 아쉬워서 외부 폰트를 설치했습니다.
  • 초반 Matplotlib 버전 업그레이드와 함께 진행했습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# Step 1. Matplotlib 업그레이드
!pip install matplotlib -U
!pip install seaborn -U
!pip install pandas -U

# Step 2. 한글 설치 및 사용 설정
!apt-get -qq install -y fonts-nanum
!fc-cache -fv
!rm ~/.cache/matplotlib -rf

# Step 3. 추가 폰트 설치
!apt-get -qq install fonts-freefont-ttf

# Step 3. 셀 실행 후 런타임 재시작
  • 라이브러리를 불러오고 환경을 설정합니다.
  • 색맹에게도 데이터를 잘 전달할 수 있도록 seaborn의 colorblind palette를 기본으로 사용합니다.
  • NanumGothic을 기본으로 지정해 그림에 한글을 출력할 수 있도록 합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# Step 4. 라이브러리 호출
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd

# Step 5. 시각화 설정
import seaborn as sns
sns.set_context("talk")
sns.set_palette("colorblind")
sns.set_style("white")

# Step 6. Linux 한글 사용 설정
plt.rcParams['font.family']=['NanumGothic', 'sans-serif']
plt.rcParams['axes.unicode_minus'] = False

2.2. 데이터 정리

  • 그림에서 데이터를 추출합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
num = 1612              # 조사 대상 수
r_ab = 0.949 # 항체 보유자 비율
r_ni = 0.157 # 자연 감염자 비율
r_vc = r_ab - r_ni # 백신 접종자(?) 비율
r_no = 1-r_ab # 항체 미보유자 비율

n_ab = int(num * r_ab) # 항체 보유자 수
n_ni = int(num * r_ni) # 자연 감염자 수
n_vc = int(num * r_vc) # 백신 접종자(?) 비율
n_no = int(num - n_ab) # 항체 미보유자 비율

df_covid = pd.DataFrame({"수": [n_ab, n_ni, n_vc, n_no],
"비율": [r_ab, r_ni, r_vc, r_no]},
index=["항체 보유자", "자연 감염자", "백신 접종자(?)", "항체 미보유자"])

df_covid


2.2. 항체 보유, 자연 감염 시각화

  • 항체 보유 94.9% 내에 자연 감염 15.7%를 포함시켜 그리기로 합니다.
  • 항체 보유와 미보유만 먼저 그린 후, 항체 미보유는 삭제합니다.
  • 해당 wedge와 text를 함께 삭제합니다.
코드 보기/접기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
fig, ax = plt.subplots(figsize=(6, 6), constrained_layout=True)

# 색상 지정
c_ah = "navy" # 항체 보유자 (antibody holder)
c_ni = "royalblue" # 자연 감염자 (naturally infested)

# pie chart : 항체 보유자
ax.pie(df_covid["수"].values[[0, 3]], # [항체 보유자, 항체 미보유자]
startangle=90, counterclock=False, # pie chart 위쪽에서 시작, 시계방향으로
colors=[c_ab], autopct="%.1f%%", pctdistance=0.8, # 색, 비율 표시 조정
wedgeprops={"width":0.5, "ec":c_ni}, # wedge properties
textprops={"fontfamily":"FreeSans", "color":"w", # text propeerties
"fontsize":50, "fontweight":"bold"})

# 항체 미보유자 부분 wedge 제거
ax.patches[-1].remove()
ax.texts[-1].remove()


  • 자연 감염, 백신 접종자(?), 항체 미보유자를 기준으로 pie chart를 새로 그리고,
  • 자연 감염자를 제외한 나머지를 제거합니다.
코드 보기/접기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
fig, ax = plt.subplots(figsize=(6, 6), constrained_layout=True)

# 색상 지정
c_ah = "navy" # 항체 보유자 (antibody holder)
c_ni = "royalblue" # 자연 감염자 (naturally infested)

# pie chart : 항체 보유자
ax.pie(df_covid["수"].values[[0, 3]], # [항체 보유자, 항체 미보유자]
startangle=90, counterclock=False, # pie chart 위쪽에서 시작, 시계방향으로
colors=[c_ab], autopct="%.1f%%", pctdistance=0.8, # 색, 비율 표시 조정
wedgeprops={"width":0.5, "ec":c_ni}, # wedge properties
textprops={"fontfamily":"FreeSans", "color":"w", # text propeerties
"fontsize":50, "fontweight":"bold"})

# 항체 미보유자 부분 wedge 제거
ax.patches[-1].remove()
ax.texts[-1].remove()

# pie chart : 자연감염
ax.pie(df_covid["수"].values[1:], # 자연 감염자 등 비율 시각화
startangle=90, counterclock=False,
colors = [c_ni], autopct="%.1f%%", pctdistance=0.7,
radius=0.95, # 전체 그림보다 5% 작게
wedgeprops={"width":0.4, "ec":c_ni},
textprops={"fontfamily":"FreeSans", "color":"w",
"fontsize":50, "fontweight":"bold"})

# 항체보유, 자연감염 이외 wedge 제거
for p in ax.patches[2:]:
p.remove()

# 항체 보유자, 자연 감염자 text외 나머지 제거
text_ab, text_ni, text_vc, text_no = [text for text in ax.texts if text.get_text() != ""]
for t in set(ax.texts) - set([text_ab, text_ni]):
t.remove()


2.3. 항체 미보유자 선 그리기

  • 원본과 마찬가지로 항체 미보유자에 선을 그립니다.
  • 곡선은 pie chart 뒤에 원을 그려 표현합니다.
코드 보기/접기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
from matplotlib.patches import Circle

fig, ax = plt.subplots(figsize=(6, 6), constrained_layout=True)

# 색상 지정
c_ah = "navy" # 항체 보유자 (antibody holder)
c_ni = "royalblue" # 자연 감염자 (naturally infested)

# pie chart : 항체 보유자
ax.pie(df_covid["수"].values[[0, 3]], # [항체 보유자, 항체 미보유자]
startangle=90, counterclock=False, # pie chart 위쪽에서 시작, 시계방향으로
colors=[c_ab], autopct="%.1f%%", pctdistance=0.8, # 색, 비율 표시 조정
wedgeprops={"width":0.5, "ec":c_ni}, # wedge properties
textprops={"fontfamily":"FreeSans", "color":"w", # text propeerties
"fontsize":50, "fontweight":"bold"})

# 항체 미보유자 부분 wedge 제거
ax.patches[-1].remove()
ax.texts[-1].remove()

# pie chart : 자연감염
ax.pie(df_covid["수"].values[1:], # 자연 감염자 등 비율 시각화
startangle=90, counterclock=False,
colors = [c_ni], autopct="%.1f%%", pctdistance=0.7,
radius=0.95, # 전체 그림보다 5% 작게
wedgeprops={"width":0.4, "ec":c_ni},
textprops={"fontfamily":"FreeSans", "color":"w",
"fontsize":50, "fontweight":"bold"})

# 항체보유, 자연감염 이외 wedge 제거
for p in ax.patches[2:]:
p.remove()

# 항체 보유자, 자연 감염자 text외 나머지 제거
text_ab, text_ni, text_vc, text_no = [text for text in ax.texts if text.get_text() != ""]
for t in set(ax.texts) - set([text_ab, text_ni]):
t.remove()

# 항체 미보유자 부분 원 표시
circle = Circle((0, 0), radius=0.75, fc="none",
lw=15, ec="0.7", zorder=-1)
ax.add_artist(circle)


2.4. 폰트 위치 및 크기 조정

  • 결국 전달하고 싶은 내용은 항체 보유자 94.9%입니다.
  • 이 안에 있는 자연 감염 15.7%는 중요한 부차 정보입니다.
  • 자연 감염은 폰트 크기를 줄이고, "자연 감염"을 위에 붙입니다.
코드 보기/접기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
from matplotlib.patches import Circle

fig, ax = plt.subplots(figsize=(6, 6), constrained_layout=True)

# 색상 지정
c_ah = "navy" # 항체 보유자 (antibody holder)
c_ni = "royalblue" # 자연 감염자 (naturally infested)

# pie chart : 항체 보유자
ax.pie(df_covid["수"].values[[0, 3]], # [항체 보유자, 항체 미보유자]
startangle=90, counterclock=False, # pie chart 위쪽에서 시작, 시계방향으로
colors=[c_ab], autopct="%.1f%%", pctdistance=0.8, # 색, 비율 표시 조정
wedgeprops={"width":0.5, "ec":c_ni}, # wedge properties
textprops={"fontfamily":"FreeSans", "color":"w", # text propeerties
"fontsize":50, "fontweight":"bold"})

# 항체 미보유자 부분 wedge 제거
ax.patches[-1].remove()
ax.texts[-1].remove()

# pie chart : 자연감염
ax.pie(df_covid["수"].values[1:], # 자연 감염자 등 비율 시각화
startangle=90, counterclock=False,
colors = [c_ni], autopct="%.1f%%", pctdistance=0.7,
radius=0.95, # 전체 그림보다 5% 작게
wedgeprops={"width":0.4, "ec":c_ni},
textprops={"fontfamily":"FreeSans", "color":"w",
"fontsize":50, "fontweight":"bold"})

# 항체보유, 자연감염 이외 wedge 제거
for p in ax.patches[2:]:
p.remove()

# 항체 보유자, 자연 감염자 text외 나머지 제거
text_ab, text_ni, text_vc, text_no = [text for text in ax.texts if text.get_text() != ""]
for t in set(ax.texts) - set([text_ab, text_ni]):
t.remove()

# 항체 미보유자 부분 원 표시
circle = Circle((0, 0), radius=0.75, fc="none",
lw=15, ec="0.7", zorder=-1)
ax.add_artist(circle)

# texts colored boundary
for t, c in zip([text_ab, text_ni], [c_ab, c_ni]):
t.set_path_effects([path_effects.Stroke(linewidth=5, foreground=c),
path_effects.SimplePatchShadow(),
path_effects.Normal()])

# 자연 감염 폰트 크기 조정
text_ni_size = text_ni.get_size()
text_ni.set_size(32)

# 자연 감염 폰트 위치
text_ni_pos = list(text_ni.get_position())

# 자연 감염 text 입력
text_ni_word = ax.text(text_ni_pos[0], text_ni_pos[1]+0.15, "자연감염", fontsize=23,
fontweight="bold", ha="center", color="w", alpha=1)
text_ni_word.set_path_effects([path_effects.Stroke(linewidth=5, foreground=c_ni),
path_effects.Normal()])


2.5. 항체 보유자 비율 이동

  • 항체 보유자 비율을 상단으로 옮깁니다.
  • "항체 있음"을 항체 보유자 비율 앞에 붙입니다.
코드 보기/접기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
from matplotlib.patches import Circle

fig, ax = plt.subplots(figsize=(6, 6), constrained_layout=True)

# 색상 지정
c_ah = "navy" # 항체 보유자 (antibody holder)
c_ni = "royalblue" # 자연 감염자 (naturally infested)

# pie chart : 항체 보유자
ax.pie(df_covid["수"].values[[0, 3]], # [항체 보유자, 항체 미보유자]
startangle=90, counterclock=False, # pie chart 위쪽에서 시작, 시계방향으로
colors=[c_ab], autopct="%.1f%%", pctdistance=0.8, # 색, 비율 표시 조정
wedgeprops={"width":0.5, "ec":c_ni}, # wedge properties
textprops={"fontfamily":"FreeSans", "color":"w", # text propeerties
"fontsize":50, "fontweight":"bold"})

# 항체 미보유자 부분 wedge 제거
ax.patches[-1].remove()
ax.texts[-1].remove()

# pie chart : 자연감염
ax.pie(df_covid["수"].values[1:], # 자연 감염자 등 비율 시각화
startangle=90, counterclock=False,
colors = [c_ni], autopct="%.1f%%", pctdistance=0.7,
radius=0.95, # 전체 그림보다 5% 작게
wedgeprops={"width":0.4, "ec":c_ni},
textprops={"fontfamily":"FreeSans", "color":"w",
"fontsize":50, "fontweight":"bold"})

# 항체보유, 자연감염 이외 wedge 제거
for p in ax.patches[2:]:
p.remove()

# 항체 보유자, 자연 감염자 text외 나머지 제거
text_ab, text_ni, text_vc, text_no = [text for text in ax.texts if text.get_text() != ""]
for t in set(ax.texts) - set([text_ab, text_ni]):
t.remove()

# 항체 미보유자 부분 원 표시
circle = Circle((0, 0), radius=0.75, fc="none",
lw=15, ec="0.7", zorder=-1)
ax.add_artist(circle)

# texts colored boundary
for t, c in zip([text_ab, text_ni], [c_ab, c_ni]):
t.set_path_effects([path_effects.Stroke(linewidth=5, foreground=c),
path_effects.SimplePatchShadow(),
path_effects.Normal()])

# 자연 감염 폰트 크기 조정
text_ni_size = text_ni.get_size()
text_ni.set_size(32)

# 자연 감염 폰트 위치
text_ni_pos = list(text_ni.get_position())

# 자연 감염 text 입력
text_ni_word = ax.text(text_ni_pos[0], text_ni_pos[1]+0.15, "자연감염", fontsize=23,
fontweight="bold", ha="center", color="w", alpha=1)
text_ni_word.set_path_effects([path_effects.Stroke(linewidth=5, foreground=c_ni),
path_effects.Normal()])

# 항체 보유 위치 조정
text_ab.set_position([0, 1.1])
text_ab.set_ha("left")
text_ab.set_va("baseline")

# 항체 보유 text 입력
text_ab_word = ax.text(-0.05, 1.17, "항체 있음", fontsize=30,
fontweight="bold",ha="right", va="center", color=c_ab)

# 총 인원 수
ax.text(0, 0, f"{num} ", fontsize=45, fontweight="bold",
fontfamily="FreeSans", ha="center", va="center", color="gray")
ax.text(0.22, -0.05, "명", fontsize=30, fontweight="bold",
ha="left", va="baseline", color="gray")


3. 결론

  • 약간의 코딩으로 왼쪽 그림을 오른쪽처럼 바꿨습니다.

  • 적어도 자연감염이 94.9% 안에 포함되어 있다는 것은 전달되는 듯 합니다.


  • 최선이라고는 생각지 않습니다.

  • 지금도 왠지 맘에 쏙 들지는 않지만 나중에 보면 마음에 더 안들지도 모릅니다.

  • 방망이를 깎는 노인의 심정으로 계속 붙들고 싶지만 현실적으로 마감 등 한계가 있습니다.

  • 기본기가 튼튼해지면 이런 제약에서도 더 좋은 결과를 낼 수 있을 것 같습니다.

  • 오늘도 방망이를 깎습니다. 같은 마음을 가진 모든 분들을 응원합니다.



도움이 되셨나요? 카페인을 투입하시면 다음 포스팅으로 변환됩니다

Share