소통을 위한 시각화 (Graphics for Communication)

서론

이 장에서는 소통을 위한 시각화 활용법을 배웁니다.

탐색적 데이터 분석 (Exploratory Data Analysis) 장에서 시각화를 탐색의 도구로 사용하는 방법을 배웠습니다. 탐색적 시각화 단계에서는 그래프를 보기도 전에 이미 어떤 변수들이 표시될지 알고 있었습니다. 명확한 목적을 가지고 각 그래프를 만들었고, 재빨리 확인한 뒤 다음 그래프로 넘어갔습니다. 대부분의 분석 과정에서 수십 개에서 수백 개의 그래프를 만들게 되지만, 그중 대다수는 즉시 폐기됩니다.

이제 데이터를 이해했으므로, 여러분의 이해를 다른 사람들에게 전달(communicate)해야 합니다. 청중은 여러분과 같은 배경지식을 공유하지 않을 가능성이 크며 데이터에 깊이 몰입해 있지도 않을 것입니다. 다른 사람들이 데이터에 대한 좋은 정신적 모델을 빠르게 구축할 수 있도록 돕기 위해서는, 그래프를 가능한 한 직관적으로 만드는 데 상당한 노력을 기울여야 합니다. 이 장에서는 차트가 이야기를 들려줄 수 있도록 lets-plot이 제공하는 몇 가지 도구를 배워보겠습니다.

사전 준비

항상 그렇듯이 코드를 사용한 데이터 시각화에는 수많은 옵션(및 패키지)이 있습니다. 여기서는 lets-plot을 사용한 선언적 “그래픽 문법(grammar of graphics)” 방식에 집중할 것이지만, 더 복잡한 그래픽을 원하는 숙련된 사용자는 훌륭한 matplotlib와 같은 명령형 라이브러리를 사용하고 싶을 수도 있습니다. lets-plotpandas가 모두 설치되어 있어야 합니다. 설치가 완료되면 다음과 같이 가져옵니다:

코드 보기
import numpy as np
import pandas as pd
from lets_plot import *

LetsPlot.setup_html()

레이블, 제목 및 기타 상황 정보

탐색적 그래픽을 설명적 그래픽으로 바꿀 때 시작하기 가장 쉬운 지점은 좋은 레이블을 붙이는 것입니다. 1999년부터 2008년까지 출시된 인기 차종 38개 모델의 연비 데이터를 담고 있는 MPG 데이터셋을 예로 들어보겠습니다.

코드 보기
# load the data
mpg = pd.read_csv(
    "https://vincentarelbundock.github.io/Rdatasets/csv/ggplot2/mpg.csv", index_col=0
)

우리는 엔진 배기량(리터 단위)에 따라 고속도로 연비가 어떻게 변하는지 보여주고 싶습니다. 이 변수들로 만들 수 있는 가장 기본적인 차트는 다음과 같습니다:

코드 보기
(ggplot(mpg, aes(x="displ", y="hwy")) + geom_point())

이제 차트를 더 돋보이게 할 유용한 정보를 많이 추가해 보겠습니다. 그래프 제목의 목적은 주요 발견 사항을 요약하는 것입니다. 단순히 그래프가 무엇인지 설명하는 제목(예: “엔진 배기량 대 연비 산점도”)은 피하십시오.

우리는 다음과 같이 할 것입니다:

  • 독자가 알아주었으면 하는 주요 발견 사항을 요약한 제목 추가 (뻔한 설명 대신!)
  • y축에 대한 더 많은 정보를 제공하는 부제목을 추가하고 x축 레이블을 더 이해하기 쉽게 수정
  • 보기 불편한 각도로 되어 있는 y축 레이블 제거
  • 데이터 출처가 포함된 캡션 추가

이 모든 것을 적용하면 다음과 같습니다:

코드 보기
(
    ggplot(mpg, aes(x="displ", y="hwy"))
    + geom_point(aes(colour="class"))
    + geom_smooth(se=False, method="loess", size=1)
    + labs(
        title="연비는 일반적으로 엔진 크기가 커짐에 따라 감소함",
        subtitle="고속도로 연비 (갤런당 마일)",
        caption="출처: fueleconomy.gov",
        y="",
        x="엔진 배기량 (리터)",
    )
)

훨씬 명확해졌습니다. 읽기 더 쉬워졌고 데이터 출처도 알 수 있으며, 왜 이 그래프를 보여주는지도 확인할 수 있습니다.

하지만 다른 메시지를 전달하고 싶다면 어떨까요? 필요에 따라 유연하게 대처할 수 있습니다. 어떤 사람들은 부제목이 더 많은 맥락을 제공할 수 있도록 y축 레이블을 회전시키는 것을 선호하기도 합니다:

코드 보기
(
    ggplot(mpg, aes(x="displ", y="hwy"))
    + geom_point(aes(colour="class"))
    + geom_smooth(se=False, method="loess", size=1)
    + labs(
        x="엔진 배기량 (L)",
        y="고속도로 연비 (mpg)",
        colour="차종",
        title="연비는 일반적으로 엔진 크기가 커짐에 따라 감소함",
        subtitle="2인승(스포츠카)은 가벼운 무게 때문에 예외에 해당함",
        caption="출처: fueleconomy.gov",
    )
)

연습 문제

  1. 연비 데이터를 사용하여 커스텀 title, subtitle, caption, x, y, color 레이블이 포함된 그래프를 하나 만드세요.

  2. 연비 데이터를 사용하여 다음 그래프를 재현해 보세요. 점의 색상과 모양이 구동 방식(drive train)에 따라 다르다는 점에 유의하세요.

  3. 지난달에 만든 탐색적 그래픽 하나를 골라, 다른 사람들이 더 이해하기 쉽도록 유익한 제목들을 추가해 보세요.

주석 (Annotations)

그래프의 주요 구성 요소에 레이블을 붙이는 것 외에도, 개별 관측치나 관측치 그룹에 레이블을 붙이는 것이 유용할 때가 많습니다. 가장 먼저 사용할 수 있는 도구는 geom_text()입니다. geom_text()geom_point()와 유사하지만, label이라는 추가적인 에스테틱(aesthetic)을 가집니다. 이를 통해 그래프에 텍스트 레이블을 추가할 수 있습니다.

레이블 소스에는 두 가지가 있습니다. 데이터의 일부인 레이블은 geom_text()로 추가하고, geom_label()을 사용하여 주석으로 직접 수동 추가할 수도 있습니다.

첫 번째 경우로, 레이블을 포함하는 데이터 프레임이 있을 수 있습니다. 다음 그래프에서는 각 구동 방식별로 가장 큰 엔진 사이즈를 가진 차량들을 추출하여 label_info라는 새 데이터 프레임에 저장합니다. 이를 생성할 때 레이블을 붙일 지점으로 “drv”별 “hwy”의 평균값을 선택했지만, 차트에서 효과적이라고 생각되는 어떤 요약 방식이든 사용할 수 있습니다.

코드 보기
mapping = {
    "4": "4륜 구동",
    "f": "전륜 구동",
    "r": "후륜 구동",
}
label_info = (
    mpg.groupby("drv")
    .agg({"hwy": "mean", "displ": "mean"})
    .reset_index()
    .assign(drive_type=lambda x: x["drv"].map(mapping))
    .round(2)
)
label_info
drv hwy displ drive_type
0 4 19.17 4.00 4륜 구동
1 f 28.16 2.56 전륜 구동
2 r 21.00 5.18 후륜 구동

그런 다음, 이 새로운 데이터 프레임을 사용하여 범례를 대신할 세 그룹의 레이블을 그래프에 직접 배치합니다. fontfacesize 인수를 사용하여 텍스트 레이블의 모양을 커스텀할 수 있습니다. 레이블들을 그래프의 나머지 텍스트보다 크게 만들고 굵게 처리했습니다. (theme(legend_position = "none")은 모든 범례를 끕니다. 이에 대해서는 잠시 후에 더 자세히 다루겠습니다.)

코드 보기
(
    ggplot(mpg, aes(x="displ", y="hwy", color="drv"))
    + geom_point(alpha=0.5)
    + geom_smooth(se=False, method="loess")
    + geom_text(
        aes(x="displ", y="hwy", label="drive_type"),
        data=label_info,
        fontface="bold",
        size=8,
        hjust="left",
        vjust="bottom",
    )
    + theme(legend_position="none")
)

레이블의 정렬을 제어하기 위해 hjust(가로 정렬)와 vjust(세로 정렬)를 사용한 점에 유의하세요.

살펴볼 두 번째 방법은 geom_label()입니다. 여기에는 두 가지 모드가 있습니다. 첫 번째는 geom_text()와 유사하게 작동하지만 다음과 같이 텍스트 주위에 상자가 그려집니다:

코드 보기
potential_outliers = mpg.query("hwy > 40 | (hwy > 20 & displ > 5)")
(
    ggplot(mpg, aes(x="displ", y="hwy"))
    + geom_point(color="black")
    + geom_smooth(se=False, method="loess", color="black")
    + geom_point(
        data=potential_outliers,
        color="red",
    )
    + geom_label(
        aes(label="model"),
        data=potential_outliers,
        color="red",
        position=position_jitter(),
        fontface="bold",
        size=5,
        hjust="left",
        vjust="bottom",
    )
    + theme(legend_position="none")
)

두 번째 방법은 다음과 같이 그래프에 하나 또는 여러 개의 주석을 추가하는 데 일반적으로 유용합니다:

코드 보기
import textwrap

# wrap the text so it is over multiple lines:
trend_text = textwrap.fill("엔진 크기가 클수록 연비가 낮아지는 경향이 있음.", 30)
trend_text
'엔진 크기가 클수록 연비가 낮아지는 경향이 있음.'
코드 보기
(
    ggplot(mpg, aes(x="displ", y="hwy"))
    + geom_point()
    + geom_label(x=3.5, y=38, label=trend_text, hjust="left", color="red")
    + geom_segment(x=2, y=40, xend=5, yend=25, arrow=arrow(type="closed"), color="red")
)

주석은 시각화의 핵심 결과물과 흥미로운 특징을 전달하는 강력한 도구입니다. 유일한 한계는 여러분의 상상력(그리고 주석을 미적으로 보기 좋게 배치하려는 여러분의 인내심)뿐입니다!

geom_text()geom_label() 외에도 lets-plot에는 그래프에 주석을 다는 데 도움이 되는 다른 많은 geom들이 있습니다. 몇 가지 아이디어:

  • 참조선을 추가하려면 geom_hline()geom_vline()을 사용하세요. 보통 선을 굵게(size = 2)와 회색(color = gray)으로 만들고, 기본 데이터 레이어 아래에 그립니다. 이렇게 하면 데이터에서 시선을 뺏지 않으면서도 쉽게 볼 수 있습니다.

  • 관심 있는 지점 주위에 사각형을 그리려면 geom_rect()를 사용하세요. 사각형의 경계는 xmin, xmax, ymin, ymax 에스테틱으로 정의됩니다.

  • 이미 보셨듯이 arrow 인수가 포함된 geom_segment()를 사용하여 화살표로 특정 지점에 주의를 끌 수 있습니다. 시작 위치를 정의하려면 xy 에스테틱을 사용하고, 끝 위치를 정의하려면 xendyend를 사용하세요.

연습 문제

  1. 무한대 위치(infinite positions)와 함께 geom_text()를 사용하여 그래프의 네 모서리에 텍스트를 배치해 보세요.

  2. 데이터 프레임을 만들지 않고 geom_label()을 사용하여 마지막 그래프의 중앙에 포인트 geom을 추가해 보세요. 포인트의 모양, 크기 또는 색상을 커스텀해 보세요.

  3. geom_text()의 레이블은 패싯(faceting)과 어떻게 상호작용하나요? 단일 패싯에만 레이블을 추가하려면 어떻게 해야 할까요? 각 패싯마다 서로 다른 레이블을 넣으려면 어떻게 해야 할까요? (힌트: geom_text()에 전달되는 데이터셋에 대해 생각해 보세요.)

  4. geom_label()에서 배경 상자의 모양을 제어하는 인수는 무엇인가요?

  5. arrow()의 네 가지 인수는 무엇인가요? 그것들은 어떻게 작동하나요? 가장 중요한 옵션들을 보여주는 일련의 그래프를 만들어 보세요.

스케일 (Scales)

소통을 위해 그래프를 개선하는 또 다른 방법은 스케일을 조정하는 것입니다. 스케일은 에스테틱 매핑이 시각적으로 어떻게 나타나는지를 제어합니다.

기본 스케일 (Default scales)

일반적으로 lets-plot은 자동으로 스케일을 추가해주므로 신경 쓸 필요가 없습니다. 예를 들어 다음을 입력하면:

(
    ggplot(mpg, aes(x="displ", y="hwy")) +
    geom_point(aes(color="class"))
)

lets-plot은 배후에서 자동으로 다음 작업을 수행합니다:

(
    ggplot(mpg, aes(x="displ", y="hwy")) +
    geom_point(aes(color="class")) +
    scale_x_continous() +
    scale_y_continuous() +
    scale_color_discrete()
)

스케일의 명명 규칙에 유의하세요: scale_ 다음에 에스테틱 이름, 그 다음 _, 그 다음 스케일 이름이 옵니다. 기본 스케일은 정렬된 변수의 유형(연속형, 이산형, 날짜시간형 또는 날짜형)에 따라 이름이 지정됩니다. scale_x_continuous()displ의 수치 데이터를 x축의 연속된 수직선 위에 배치하고, scale_color_discrete()는 자동차의 각 class에 맞는 색상을 선택하는 식입니다. 기본값이 아닌 다른 스케일들도 많이 있으며 아래에서 배우게 될 것입니다.

기본 스케일은 광범위한 입력에 대해 잘 작동하도록 신중하게 선택되었습니다. 그럼에도 불구하고 두 가지 이유로 기본값을 재정의하고 싶을 수 있습니다:

  • 기본 스케일의 일부 파라미터를 미세 조정하고 싶을 때입니다. 이를 통해 축의 눈금(breaks)을 변경하거나 범례의 키 레이블을 변경하는 등의 작업을 할 수 있습니다.

  • 스케일을 완전히 다른 알고리즘을 사용하는 것으로 교체하고 싶을 때입니다. 데이터에 대해 더 많이 알고 있다면 종종 기본값보다 더 나은 결과를 얻을 수 있습니다.

축 눈금과 범례 키 (Axis ticks and legend keys)

축과 범례를 통칭하여 lets-plot에서는 다소 혼란스럽게도 가이드(guides)라고 부릅니다. 축은 x 및 y 에스테틱에 사용되고, 범례는 그 외의 모든 것에 사용됩니다.

축의 눈금(ticks)과 범례의 키(keys)의 모양에 영향을 주는 두 가지 주요 인수는 breakslabels입니다. Breaks는 눈금의 위치 또는 키와 관련된 값을 제어합니다. 말하자면 breaks가 곧 눈금입니다. Labels는 각 눈금/키와 관련된 텍스트 레이블을 제어합니다. 이를 더 정확하게 눈금 레이블(tick labels)이라고 부를 수 있습니다. breaks의 가장 일반적인 용도는 기본 선택값을 재정의하는 것입니다:

코드 보기
(
    ggplot(mpg, aes(x="displ", y="hwy", color="drv"))
    + geom_point()
    + scale_y_continuous(breaks=np.arange(15, 40, step=5))
)

labels도 같은 방식으로 사용할 수 있습니다 (즉, breaks와 길이가 같은 문자열 배열이나 리스트를 전달합니다). 하지만 이를 완전히 제거하려면 테마(theme)를 사용해야 하며, 이 주제는 나중에 다시 다루겠습니다. breakslabels를 사용하여 범례의 모양을 제어할 수도 있습니다. 범주형 변수에 대한 이산형 스케일의 경우, labels는 기존 레벨 이름과 원하는 레이블 이름의 명명된 리스트가 될 수 있습니다.

코드 보기
(
    ggplot(mpg, aes(x="displ", y="hwy", color="drv"))
    + geom_point()
    + scale_color_discrete(labels=["4륜", "전륜", "후륜"])
)

눈금 레이블의 서식을 변경하려면 format= 키워드 인수를 사용하세요. 통화, 백분율 등을 표시하는 데 유용합니다. 비록 독자가 축 레이블에서 이 기호를 한 번만 보는 것이 더 쉬울 때가 많지만요.

아래 예제에서는 diamonds 데이터셋을 읽어온 다음 format="$.2s" 명령으로 서식을 지정합니다. 이를 분석해 보겠습니다:

  • 달러 기호($)는 모든 숫자 앞에 달러 기호를 붙이라는 의미입니다.
  • .2는 유효숫자 두 자리를 사용하라는 의미입니다.
  • s는 국제 단위계(SI)를 사용하라는 의미입니다.

서식 지정을 위한 수많은 대안 옵션이 있습니다. 더 자세히 알아보려면 lets-plot 문서의 서식 지정 관련 페이지를 참고하는 것이 좋습니다.

코드 보기
diamonds = pd.read_csv(
    "https://vincentarelbundock.github.io/Rdatasets/csv/ggplot2/diamonds.csv",
    index_col=0,
)
diamonds["cut"] = diamonds["cut"].astype(
    pd.CategoricalDtype(
        categories=["Fair", "Good", "Very Good", "Premium", "Ideal"], ordered=True
    )
)
diamonds["color"] = diamonds["color"].astype(
    pd.CategoricalDtype(categories=["D", "E", "F", "G", "H", "I", "J"], ordered=True)
)
코드 보기
(
    ggplot(diamonds, aes(x="cut", y="price"))
    + geom_boxplot()
    + coord_flip()
    + scale_y_continuous(format="$.2s", breaks=np.arange(0, 19000, step=6000))
)

눈금(breaks)의 또 다른 용도는 데이터 포인트가 비교적 적고 관측치가 정확히 어디에서 발생하는지 강조하고 싶을 때입니다. 예를 들어, 각 미국 대통령의 임기 시작과 종료를 보여주는 이 그래프를 보십시오.

코드 보기
presidential = pd.read_csv(
    "https://vincentarelbundock.github.io/Rdatasets/csv/ggplot2/presidential.csv",
    index_col=0,
)
presidential = presidential.astype({"start": "datetime64[ns]", "end": "datetime64[ns]"})
presidential["id"] = 33 + presidential.index
presidential.head()
name start end party id
rownames
1 Eisenhower 1953-01-20 1961-01-20 Republican 34
2 Kennedy 1961-01-20 1963-11-22 Democratic 35
3 Johnson 1963-11-22 1969-01-20 Democratic 36
4 Nixon 1969-01-20 1974-08-09 Republican 37
5 Ford 1974-08-09 1977-01-20 Republican 38
코드 보기
(
    ggplot(presidential, aes(x="start", y="id"))
    + geom_point()
    + geom_segment(aes(xend="end", yend="id"))
    + scale_x_datetime()
)

범례 레이아웃 (Legend layout)

대부분 축을 조정할 때 breakslabels를 사용하게 될 것입니다. 이 두 가지 모두 범례에서도 작동하지만, 실제로는 다른 기술들을 더 많이 사용하게 될 가능성이 큽니다.

범례의 전체적인 위치를 제어하려면 theme() 설정을 사용해야 합니다. 테마는 이 장의 끝부분에서 다시 다루겠지만, 간단히 말해 그래프의 데이터 이외의 부분을 제어합니다. 테마 설정인 legend.position은 범례가 그려지는 위치를 제어하며, 이를 시연하기 위해 gggrid()를 사용하여 모든 그래프를 배치해 보겠습니다.

코드 보기
base = ggplot(mpg, aes(x="displ", y="hwy")) + geom_point(aes(color="class"))

p1 = base + theme(legend_position="right")  # the default
p2 = base + theme(legend_position="left")
p3 = base + theme(legend_position="top") + guides(color=guide_legend(nrow=3))
p4 = base + theme(legend_position="bottom") + guides(color=guide_legend(nrow=3))

gggrid([p1, p2, p3, p4], ncol=2)

그래프가 가로로 길고 높이가 낮다면 범례를 위나 아래에 배치하고, 세로로 길고 너비가 좁다면 범례를 왼쪽이나 오른쪽에 배치하세요. 또한 legend_position = "none"을 사용하여 범례 표시를 아예 억제할 수도 있습니다.

개별 범례의 표시를 제어하려면 guides()guide_legend() 또는 guide_colorbar()와 함께 사용하세요.

스케일 교체하기 (Replacing a scale)

세부 사항을 조금 조정하는 대신 스케일을 아예 통째로 교체할 수도 있습니다. 가장 교체하고 싶을 가능성이 큰 스케일은 연속형 위치 스케일과 색상 스케일입니다. 다행히 동일한 원칙이 다른 모든 에스테틱에도 적용되므로, 위치와 색상을 마스터하고 나면 다른 스케일 교체 방법도 빠르게 익힐 수 있습니다.

변수를 변환하여 그래프를 그리는 것은 매우 유용합니다. 예를 들어, caratprice를 로그 변환하면 이들 사이의 정확한 관계를 더 쉽게 볼 수 있습니다. 이를 수행하는 방법은 ggplot에 전달되는 데이터에 apply() 함수를 사용하는 것입니다:

코드 보기
(
    ggplot(
        diamonds.apply({"carat": np.log10, "price": np.log10}),
        aes(x="carat", y="price"),
    )
    + geom_bin2d()
)

하지만 이 변환의 단점은 축에 원래 값이 아닌 변환된 값이 레이블로 붙어 있어 그래프를 해석하기 어렵다는 것입니다. 에스테틱 매핑에서 변환을 수행하는 대신 스케일을 사용하여 변환을 수행할 수 있습니다. 이것은 시각적으로는 동일하지만 축 레이블은 원래 데이터 스케일로 표시됩니다.

코드 보기
(
    ggplot(diamonds, aes(x="carat", y="price"))
    + geom_bin2d()
    + scale_x_log10()
    + scale_y_log10()
)

자주 커스텀하게 되는 또 다른 스케일은 색상입니다. 기본 범주형 스케일은 색상 휠 주위에 균등하게 배치된 색상을 선택합니다. 유용한 대안은 흔한 유형의 색맹이 있는 사람들에게 더 잘 작동하도록 수동으로 조정된 ColorBrewer 스케일입니다. 아래의 두 그래프는 비슷해 보이지만, 오른쪽 그래프의 빨간색과 녹색 색조 차이가 충분하여 적록색맹이 있는 사람들도 점들을 구분할 수 있습니다.

코드 보기
(ggplot(mpg, aes(x="displ", y="hwy")) + geom_point(aes(color="drv")))
코드 보기
(
    ggplot(mpg, aes(x="displ", y="hwy"))
    + geom_point(aes(color="drv"))
    + scale_color_brewer(palette="Set1")
)

접근성을 높이기 위한 더 간단한 기법들도 잊지 마세요. 색상이 몇 개뿐이라면 중복된 모양(shape) 매핑을 추가할 수 있습니다. 이는 그래프가 흑백으로 출력되었을 때도 해석 가능하도록 보장하는 데 도움이 됩니다.

ColorBrewer 스케일은 https://colorbrewer2.org/에 문서화되어 있습니다. 순차적(sequential, 위쪽) 및 발산적(diverging, 아래쪽) 팔레트는 범주형 값이 정렬되어 있거나 “중간값”이 있는 경우에 특히 유용합니다. 이는 pd.cut()을 사용하여 연속형 변수를 범주형 변수로 만들었을 때 자주 발생합니다.

코드 보기
# remove-input
cmaps = [
    (
        "Perceptually Uniform Sequential",
        ["viridis", "plasma", "inferno", "magma", "cividis"],
    ),
    (
        "Sequential",
        [
            "Blues",
            "BuGn",
            "BuPu",
            "GnBu",
            "Greens",
            "Greys",
            "Oranges",
            "OrRd",
            "PuBu",
            "PuBuGn",
            "PuRd",
            "Purples",
            "RdPu",
            "Reds",
            "YlGn",
            "YlGnBu",
            "YlOrBr",
            "YlOrRd",
        ],
    ),
    (
        "Diverging",
        [
            "BrBG",
            "PiYG",
            "PRGn",
            "PuOr",
            "RdBu",
            "RdGy",
            "RdYlBu",
            "RdYlGn",
        ],
    ),
    (
        "Qualitative",
        [
            "Pastel1",
            "Pastel2",
            "Paired",
            "Accent",
            "Dark2",
            "Set1",
            "Set2",
            "Set3",
            "tab10",
            "tab20",
            "tab20b",
            "tab20c",
        ],
    ),
]


gradient = np.linspace(0, 1, 256)
gradient = np.vstack((gradient, gradient))


def plot_color_gradients(cmap_category, cmap_list):
    # Create figure and adjust figure height to number of colourmaps
    nrows = len(cmap_list)
    figh = 0.35 + 0.15 + (nrows + (nrows - 1) * 0.1) * 0.22
    fig, axs = plt.subplots(nrows=nrows, figsize=(6.4, figh))
    fig.subplots_adjust(top=1 - 0.35 / figh, bottom=0.15 / figh, left=0.2, right=0.99)

    axs[0].set_title(cmap_category + " colormaps", fontsize=14)

    for ax, name in zip(axs, cmap_list):
        ax.imshow(gradient, aspect="auto", cmap=plt.get_cmap(name))
        ax.text(
            -0.01,
            0.5,
            name,
            va="center",
            ha="right",
            fontsize=10,
            transform=ax.transAxes,
        )

    # Turn off *all* ticks & spines, not just the ones with colourmaps.
    for ax in axs:
        ax.set_axis_off()


for cmap_category, cmap_list in cmaps[1:2]:
    plot_color_gradients(cmap_category, cmap_list)

plt.show()

코드 보기
# remove input
for cmap_category, cmap_list in cmaps[3:4]:
    plot_color_gradients(cmap_category, cmap_list)

코드 보기
# remove input
for cmap_category, cmap_list in cmaps[2:3]:
    plot_color_gradients(cmap_category, cmap_list)

값과 색상 사이에 사전 정의된 매핑이 있는 경우 scale_color_manual()을 사용하세요. 예를 들어 대통령 소속 정당을 색상에 매핑한다면, 공화당에는 빨간색, 민주당에는 파란색을 사용하는 표준 매핑을 사용하고 싶을 것입니다. 이러한 색상을 지정하는 한 가지 방법은 16진수 색상 코드를 사용하는 것입니다:

코드 보기
mini_presid = presidential.iloc[5:, :]

(
    ggplot(mini_presid, aes(x="start", y="id", color="party"))
    + geom_point(size=3)
    + geom_segment(aes(xend="end", yend="id"), size=1)
    + scale_x_datetime(breaks=mini_presid["start"], format="%Y")
    + scale_color_manual(values=["#00AEF3", "#E81B23"], name="정당")
)

“red”나 “blue”와 같은 일반적인 색상 이름을 사용할 수도 있습니다.

연속형 색상의 경우, 내장된 scale_color_gradient() 또는 scale_fill_gradient()를 사용할 수 있습니다. 발산형 스케일이 있는 경우에는 scale_color_gradient2()를 사용할 수 있습니다. 이를 통해 예를 들어 양수 값과 음수 값에 서로 다른 색상을 부여할 수 있습니다. 이는 평균보다 높거나 낮은 점들을 구분하고 싶을 때도 유용합니다.

또 다른 옵션은 매우 강력한 명령형 파이썬 프로팅 패키지인 matplotlib을 위해 개발된 viridis, magma, inferno, plasma 색상 스케일을 사용하는 것입니다. 디자이너인 Nathaniel Smith와 Stéfan van der Walt는 다양한 형태의 색맹이 있는 사람들도 인식할 수 있을 뿐만 아니라, 컬러와 흑백 모두에서 지각적으로 균일한 연속형 색상 체계를 정교하게 만들었습니다. 이러한 스케일들은 lets-plot에서 팔레트로 사용할 수 있습니다. 다음은 viridis의 연속형 버전을 사용한 예입니다 (먼저 몇 가지 랜덤 데이터를 생성하겠습니다):

코드 보기
prng = np.random.default_rng(1837)  # prng=probabilistic random number generator
df_rnd = pd.DataFrame(prng.standard_normal((1000, 2)), columns=["x", "y"])
(
    ggplot(df_rnd, aes(x="x", y="y"))
    + geom_bin2d()
    + coord_fixed()
    + scale_fill_viridis(option="plasma")
    + labs(title="Plasma, 연속형")
)

확대/축소 (Zooming)

그래프의 범위를 제어하는 데는 세 가지 방법이 있습니다:

  1. 플롯할 데이터를 조정합니다.
  2. 각 스케일에서 제한(limits)을 설정합니다.
  3. coord_cartesian()에서 xlimylim을 설정합니다.

일련의 그래프들을 통해 이 옵션들을 시연해 보겠습니다. 첫 번째 그래프는 구동 방식별로 색상이 지정된 엔진 크기와 연비 사이의 관계를 보여줍니다. 두 번째 그래프는 동일한 변수들을 보여주지만, 플롯되는 데이터의 부분 집합만 추출합니다. 데이터를 서브세팅하면 x축과 y축의 스케일뿐만 아니라 매끄러운 곡선(smooth curve)에도 영향을 줍니다.

코드 보기
(
    ggplot(mpg, aes(x="displ", y="hwy"))
    + geom_point(aes(color="drv"))
    + geom_smooth(method="loess")
)
코드 보기
mpg_condition = (
    (mpg["displ"] >= 5) & (mpg["displ"] <= 6) & (mpg["hwy"] >= 10) & (mpg["hwy"] <= 25)
)

(
    ggplot(mpg.loc[mpg_condition], aes(x="displ", y="hwy"))
    + geom_point(aes(color="drv"))
    + geom_smooth(method="loess")
)

이를 개별 스케일에서 limits를 설정한 첫 번째 그래프와 coord_cartesian()에서 설정한 두 번째 그래프와 비교해 보겠습니다. 제한(limits)을 줄이는 것은 데이터를 서브세팅하는 것과 동일한 효과를 냄을 알 수 있습니다. 따라서 그래프의 특정 영역을 확대하려면 일반적으로 coord_cartesian()을 사용하는 것이 가장 좋습니다.

코드 보기
(
    ggplot(mpg, aes(x="displ", y="hwy"))
    + geom_point(aes(color="drv"))
    + geom_smooth(method="loess")
    + scale_x_continuous(limits=(5, 6))
    + scale_y_continuous(limits=(10, 25))
)
코드 보기
(
    ggplot(mpg, aes(x="displ", y="hwy"))
    + geom_point(aes(color="drv"))
    + geom_smooth(method="loess")
    + coord_cartesian(xlim=(5, 6), ylim=(10, 25))
)

반면, 개별 스케일에서 limits를 설정하는 것은 여러 그래프 간에 스케일을 맞추기 위해 제한을 확장하고 싶을 때 일반적으로 더 유용합니다. 예를 들어, 두 종류의 차량 클래스만 추출하여 따로 그리면, 세 가지 스케일(x축, y축, 색상 에스테틱)이 모두 서로 다른 범위를 가지기 때문에 그래프들을 비교하기 어렵습니다.

코드 보기
suv = mpg.loc[mpg["class"] == "suv"]
compact = mpg.loc[mpg["class"] == "compact"]
(ggplot(suv, aes(x="displ", y="hwy", color="drv")) + geom_point())
코드 보기
(ggplot(compact, aes(x="displ", y="hwy", color="drv")) + geom_point())

이 문제를 해결하는 한 가지 방법은 전체 데이터의 limits를 사용하여 스케일을 학습시켜 여러 그래프 간에 스케일을 공유하는 것입니다.

코드 보기
x_scale = scale_x_continuous(limits=mpg["displ"].agg(["max", "min"]).tolist())
y_scale = scale_y_continuous(limits=mpg["hwy"].agg(["max", "min"]).tolist())
col_scale = scale_color_discrete(limits=mpg["drv"].unique())
코드 보기
(
    ggplot(suv, aes(x="displ", y="hwy", color="drv"))
    + geom_point()
    + x_scale
    + y_scale
    + col_scale
)
코드 보기
(
    ggplot(compact, aes(x="displ", y="hwy", color="drv"))
    + geom_point()
    + x_scale
    + y_scale
    + col_scale
)

이 특별한 경우에는 단순히 패싯(faceting)을 사용할 수도 있었겠지만, 이 기술은 예를 들어 리포트의 여러 페이지에 걸쳐 그래프를 분산 배치하고 싶을 때처럼 더 일반적인 상황에서 유용합니다.

연습 문제

  1. 모든 스케일의 첫 번째 인수는 무엇인가요? 그것은 labs()와 어떻게 비교되나요?

  2. 대통령 임기 표시를 다음과 같이 변경해 보세요:

    1. 색상과 x축 눈금을 커스텀하는 두 변형을 결합합니다.
    2. y축 표시를 개선합니다.
    3. 각 임기에 대통령 이름을 레이블로 붙입니다.
    4. 유익한 그래프 레이블을 추가합니다.
    5. 눈금을 4년마다 배치합니다 (생각보다 까다롭습니다!).

테마 (Themes)

마지막으로, 테마를 사용하여 그래프의 데이터 이외의 요소들을 커스텀할 수 있습니다:

코드 보기
(
    ggplot(mpg, aes(x="displ", y="hwy"))
    + geom_point(aes(color="class"))
    + geom_smooth(se=False)
    + theme_grey()
)

lets-plot에는 여기에서 확인할 수 있는 몇 가지 내장 테마가 포함되어 있습니다. 또한 특정 기업이나 저널의 스타일에 맞추기 위해 자신만의 테마를 만들 수도 있습니다.

다음은 여러 theme() 설정을 변경하는 예제입니다:

코드 보기
(
    ggplot(mpg, aes(x="displ", color="drv"))
    + geom_density(size=2)
    + ggtitle("구동 방식의 밀도")
    + theme(
        axis_line=element_line(size=4),
        axis_ticks_length=10,
        axis_title_y="blank",
        legend_position=[1, 1],
        legend_justification=[1, 1],
        panel_background=element_rect(color="black", fill="#eeeeee", size=2),
        panel_grid=element_line(color="black", size=1),
    )
)

연습 문제

  1. 그래프의 축 레이블을 파란색이고 굵게 만들어 보세요.

레이아웃 (Layout)

지금까지는 단일 그래프를 생성하고 수정하는 방법에 대해 이야기했습니다. 여러 개의 그래프를 특정한 방식으로 배치하고 싶다면 어떻게 해야 할까요? 가능합니다. 두 그래프를 나란히 배치하려면 단순히 리스트에 넣고 그 리스트에 gggrid()를 호출하면 됩니다. 먼저 그래프를 생성하고 객체로 저장해야 한다는 점에 유의하세요 (다음 예제에서는 p1p2라고 부릅니다).

코드 보기
p1 = ggplot(mpg, aes(x="displ", y="hwy")) + geom_point() + labs(title="그래프 1")
p2 = ggplot(mpg, aes(x="drv", y="hwy")) + geom_boxplot() + labs(title="그래프 2")
gggrid([p1, p2])

그래프를 파일로 저장하기

파일을 저장할 때 선택할 수 있는 출력 옵션이 많습니다. 그래픽의 경우 일반적으로 벡터 포맷(vector formats)래스터 포맷(raster formats)보다 낫다는 점을 기억하세요. 실제로는 jpg나 png 파일 포맷보다 svg나 pdf 포맷으로 그래프를 저장하는 것을 의미합니다. svg 포맷은 많은 맥락에서 잘 작동하며(Microsoft Word 포함) 좋은 기본 선택입니다. 포맷을 선택하려면 파일 확장자만 제공하면 파일 타입이 자동으로 변경됩니다. 예: svg는 “chart.svg”, png는 “chart.png” (단, 래스터 포맷은 인치당 도트 수(dpi)와 같은 추가 옵션이 있는 경우가 많습니다).

이전 연습에서 만든 피겨 p1을 사용하여 이를 시도해 보겠습니다. path="."은 단순히 현재 디렉토리에 파일을 생성합니다.

코드 보기
ggsave(p1, "chart.svg", path=".")
'/home/runner/work/python4DS/python4DS/mybook/chart.svg'

이것이 제대로 작동했는지 다시 확인하기 위해 터미널을 사용해 보겠습니다. 디렉토리의 모든 것을 나열하는 ls 명령과, ls가 반환한 것 중에서 .svg로 끝나는 파일만 골라내는 grep *.svg를 사용하겠습니다. 이것들은 |로 연결되어 명령어로 실행됩니다. (아래의 맨 앞 느낌표는 이 책을 빌드하는 소프트웨어에게 터미널을 사용하라고 알려주는 것뿐입니다.)

코드 보기
!ls | grep *.svg
chart.svg

요약

이 장에서는 제목, 부제목, 캡션과 같은 그래프 레이블을 추가하는 방법과 기본 축 레이블을 수정하는 방법, 주석을 사용하여 그래프에 정보성 텍스트를 추가하거나 특정 데이터 포인트를 강조하는 방법, 축 스케일을 커스텀하는 방법, 그래프의 테마를 변경하는 방법 등을 배웠습니다. 또한 단순하거나 복잡한 플롯 레이아웃을 모두 사용하여 단일 그래프에 여러 개의 플롯을 결합하는 방법도 배웠습니다.

지금까지 여러 유형의 그래프를 만드는 방법과 다양한 기법을 사용하여 이를 커스텀하는 방법을 배웠지만, 우리는 lets-plot으로 만들 수 있는 것들의 겉면만 살짝 긁어본 것에 불과합니다.

더 자세한 정보는 lets-plot 문서를 참고하는 것이 가장 좋습니다.