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