데이터 시각화 (Data Visualisation)

서론

“간단한 그래프 하나가 다른 어떤 장치보다 데이터 분석가의 마음에 더 많은 정보를 가져다주었다.” — 존 터키(John Tukey)

이 장에서는 letsplot을 사용하여 데이터를 시각화하는 방법을 배웁니다.

코드를 사용하여 데이터 시각화를 만드는 방식에는 크게 두 가지 카테고리가 있습니다: 원하는 것을 직접 구축하는 명령형(imperative) 방식과, 원하는 것을 말하는 선언적(declarative) 방식입니다. 어떤 것을 선택할지는 트레이드오프 관계에 있습니다. 명령형 라이브러리는 유연성을 제공하지만 코드가 장황해질 수 있습니다. 선언적 라이브러리는 데이터를 플롯하는 빠른 방법을 제공하지만, 데이터가 처음부터 올바른 형식이어야 하며 특수한 차트 유형으로 커스텀하기가 더 어렵습니다. 파이썬에는 아마도 가장 강력한 명령형 시각화 패키지인 matplotlib를 포함하여 훌륭한 시각화 패키지가 많습니다.

하지만 우리는 하나의 시스템을 배우고 이를 여러 곳에 적용함으로써 더 빨리 나아갈 수 있습니다. 선언적 시각화의 아름다움은 표준 차트들을 간단하고 훌륭하게 처리한다는 점입니다. letsplot은 그래프를 설명하고 구축하기 위한 일관된 선언적 시스템인 소위 그래픽 문법(grammar of graphics)을 구현합니다.

먼저 간단한 산점도(scatterplot)를 만드는 것으로 시작하여, letsplot의 기본 구성 요소인 에스테틱 매핑(aesthetic mappings)과 기하 객체(geometric objects)를 소개하겠습니다. 그런 다음 단일 변수의 분포를 시각화하는 방법과 두 개 이상의 변수 간의 관계를 시각화하는 방법을 단계별로 안내하겠습니다. 마지막으로 그래프를 저장하는 방법과 문제 해결 팁으로 마무리하겠습니다.

사전 준비

이 장에서는 letsplot 패키지를 설치해야 합니다. 설치하려면 컴퓨터의 명령줄(터미널)을 열고 pixi add lets-plot을 입력한 뒤 엔터를 누르세요.

명령줄(터미널)은 Visual Studio Code나 Codespaces에서 View -> Terminal로 이동하여 열 수 있습니다.

각 파이썬 환경에서 패키지는 한 번만 설치하면 된다는 점을 기억하세요.

또한 앞으로 많이 보게 될 데이터 처리를 위한 pandas 패키지도 설치되어 있어야 합니다. 마찬가지로 명령줄에서 pixi add pandas를 실행하여 설치할 수 있습니다.

마지막으로 데이터도 필요합니다(데이터 없이는 과학을 할 수 없습니다). 우리는 팔머 펭귄(Palmer penguins) 데이터셋을 사용할 것입니다. 특이하게도 이 데이터셋은 패키지로 설치할 수 있습니다. 튜토리얼용으로 매우 인기가 많아 설치 가능한 패키지로 만들어졌기 때문입니다. pixi add palmerpenguins를 실행하여 이 데이터를 가져오세요.

다음 작업은 이들을 파이썬 세션으로 불러오는 것입니다. 주피터 노트북의 파이썬 셀에 작성하거나, 스크립트에 작성한 뒤 대화형 창(interactive window)으로 보내거나, 대화형 창에 직접 입력하고 Shift+Enter를 누르면 됩니다. 코드는 다음과 같습니다:

코드 보기
from lets_plot import *
from palmerpenguins import load_penguins

LetsPlot.setup_html()

위 코드들은 pandaspalmerpenguins 패키지의 일부를 가져오고, letsplot 패키지의 모든(*) 함수를 가져옵니다. 마지막 줄은 차트가 HTML로 표시되도록 설정합니다.

첫 단계

지느러미(flipper)가 더 긴 펭귄이 지느러미가 짧은 펭귄보다 몸무게가 더 많이 나갈까요, 적게 나갈까요? 아마 이미 답을 알고 계시겠지만, 그 답을 정확하게 만들어 보십시오. 지느러미 길이와 몸무게 사이의 관계는 어떻게 생겼을까요? 양의 관계일까요? 음의 관계일까요? 선형일까요, 비선형일까요? 이 관계는 펭귄의 종(species)에 따라 달라질까요? 펭귄이 사는 섬(island)에 따라서는 어떨까요? 이러한 질문들에 답하기 위해 시각화를 만들어 보겠습니다.

penguins 데이터 프레임

palmerpenguins 패키지에 있는 펭귄 데이터 프레임(from palmerpenguins import load_penguins)으로 질문들을 테스트해 볼 수 있습니다. 데이터 프레임은 변수(열)와 관측치(행)의 직사각형 컬렉션입니다. penguins에는 Kristen Gorman 박사와 남극 팔머 역(Palmer Station) LTER에서 수집하여 제공한 344개의 관측치가 포함되어 있습니다.(Horst, Hill, and Gorman 2020).

논의를 쉽게 하기 위해 몇 가지 용어를 정의해 보겠습니다:

  • 변수(variable)는 측정할 수 있는 양, 질 또는 속성입니다.

  • 값(value)은 변수를 측정할 때의 상태입니다. 변수의 값은 측정할 때마다 바뀔 수 있습니다.

  • 관측치(observation)는 유사한 조건에서 이루어진 측정값들의 집합입니다(보통 한 번에 동일한 대상에 대해 모든 측정을 수행합니다). 하나의 관측치에는 서로 다른 변수와 관련된 여러 개의 값이 포함됩니다. 관측치를 데이터 포인트라고 부르기도 합니다.

  • 표 형태 데이터(tabular data)는 각각 변수 및 관측치와 연관된 값들의 집합입니다. 표 형태 데이터가 깔끔하다(tidy)는 것은 각 값이 고유한 “셀”에 있고, 각 변수가 고유한 열에 있으며, 각 관측치가 고유한 행에 있는 경우를 말합니다.

이 문맥에서 변수는 모든 펭귄의 속성을 의미하고, 관측치는 단일 펭귄의 모든 속성을 의미합니다.

대화형 창에 데이터 프레임의 이름을 입력하면 파이썬이 그 내용의 미리보기를 출력할 것입니다.

코드 보기
penguins = load_penguins()
penguins
species island bill_length_mm bill_depth_mm flipper_length_mm body_mass_g sex year
0 Adelie Torgersen 39.1 18.7 181.0 3750.0 male 2007
1 Adelie Torgersen 39.5 17.4 186.0 3800.0 female 2007
2 Adelie Torgersen 40.3 18.0 195.0 3250.0 female 2007
3 Adelie Torgersen NaN NaN NaN NaN NaN 2007
4 Adelie Torgersen 36.7 19.3 193.0 3450.0 female 2007
... ... ... ... ... ... ... ... ...
339 Chinstrap Dream 55.8 19.8 207.0 4000.0 male 2009
340 Chinstrap Dream 43.5 18.1 202.0 3400.0 female 2009
341 Chinstrap Dream 49.6 18.2 193.0 3775.0 male 2009
342 Chinstrap Dream 50.8 19.0 210.0 4100.0 male 2009
343 Chinstrap Dream 50.2 18.7 198.0 3775.0 female 2009

344 rows × 8 columns

각 변수의 처음 몇 가지 관측치를 볼 수 있는 다른 뷰를 원한다면 penguins.head()를 사용하세요.

코드 보기
penguins.head()
species island bill_length_mm bill_depth_mm flipper_length_mm body_mass_g sex year
0 Adelie Torgersen 39.1 18.7 181.0 3750.0 male 2007
1 Adelie Torgersen 39.5 17.4 186.0 3800.0 female 2007
2 Adelie Torgersen 40.3 18.0 195.0 3250.0 female 2007
3 Adelie Torgersen NaN NaN NaN NaN NaN 2007
4 Adelie Torgersen 36.7 19.3 193.0 3450.0 female 2007

penguins에 포함된 변수들은 다음과 같습니다:

  1. species: 펭귄의 종 (Adelie, Chinstrap, Gentoo).

  2. flipper_length_mm: 펭귄 지느러미의 길이 (밀리미터 단위).

  3. body_mass_g: 펭귄의 몸무게 (그램 단위).

penguins에 대해 더 알고 싶다면, 데이터 로딩 함수의 도움말 페이지를 실행해 보세요(help(load_penguins)).

궁극적인 목표

이 장에서 우리의 궁극적인 목표는 펭귄의 종을 고려하여 지느러미 길이와 몸무게 사이의 관계를 보여주는 다음 시각화를 재현하는 것입니다.

코드 보기
(
    ggplot(penguins, aes(x="flipper_length_mm", y="body_mass_g"))
    + geom_point(aes(color="species", shape="species"))
    + geom_smooth(method="lm")
    + labs(
        title="몸무게와 지느러미 길이",
        subtitle="아델리, 턱끈, 젠투 펭귄의 치수",
        x="지느러미 길이 (mm)",
        y="몸무게 (g)",
        color="종",
        shape="종",
    )
)

그래프 만들기

이 그래프를 단계별로 재현해 보겠습니다.

letsplot을 사용하면 ggplot() 함수로 그래프를 시작하며, 먼저 플롯 객체를 정의한 다음 여기에 레이어(layers)를 추가합니다.

ggplot()의 첫 번째 인수는 그래프에서 사용할 데이터셋이므로, ggplot(data = penguins)penguins 데이터를 표시할 준비가 된 빈 그래프를 생성합니다. 하지만 아직 어떻게 시각화할지 말해주지 않았기 때문에 지금은 비어 있습니다. 비어 있기 때문에 이것만 실행하면 에러 메시지가 뜰 것입니다. 이것은 여러분이 나머지 레이어들을 그려 넣을 빈 캔버스입니다.

ggplot(data = penguins)

다음으로, 데이터의 정보가 시각적으로 어떻게 표현될지 ggplot()에 알려주어야 합니다.

ggplot() 함수의 mapping 인수는 데이터셋의 변수들이 그래프의 시각적 속성(에스테틱, aesthetics)에 어떻게 매핑될지를 정의합니다. mapping 인수는 항상 aes() 함수 안에서 정의되며, aes()xy 인수는 x축과 y축에 매핑할 변수를 지정합니다. 지금은 지느러미 길이를 x 에스테틱에, 몸무게를 y 에스테틱에 매핑하겠습니다. letsplotdata 인수에 지정된 penguins에서 매핑된 변수들을 찾습니다.

다시 말씀드리지만, 아직 플롯할 대상을 구체적으로 지정하지 않았으므로 다음 코드를 실행하면

ggplot(
  data = penguins,
  mapping = aes(x = "flipper_length_mm", y = "body_mass_g")
)

에러가 발생할 것입니다. 이는 데이터 프레임의 관측치를 그래프에 어떻게 표현할지 코드로 명시하지 않았기 때문입니다.

그렇게 하려면 그래프가 데이터를 표현하는 데 사용하는 기하학적 객체인 geom을 정의해야 합니다. 이러한 기하학적 객체들은 letsplot에서 geom_으로 시작하는 함수들을 통해 제공됩니다.

사람들은 종종 그래프가 사용하는 geom의 유형으로 그래프를 설명합니다. 예를 들어 막대 그래프는 막대 geom(geom_bar())을 사용하고, 선 그래프는 선 geom(geom_line()), 박스 플롯은 박스 플롯 geom(geom_boxplot()), 산점도는 포인트 geom(geom_point())을 사용하는 식입니다.

함수 geom_point()는 그래프에 점 레이어를 추가하여 산점도를 만듭니다. letsplot에는 각각 다른 유형의 레이어를 추가하는 많은 geom 함수들이 있습니다.

코드 보기
(
    ggplot(data=penguins, mapping=aes(x="flipper_length_mm", y="body_mass_g"))
    + geom_point()
)

이제 우리가 “산점도”라고 생각하는 모습의 결과물이 나왔습니다. 아직 우리의 “궁극적인 목표”인 그래프와 일치하지는 않지만, 이 그래프를 통해 탐색의 동기가 되었던 질문에 답하기 시작할 수 있습니다: “지느러미 길이와 몸무게 사이의 관계는 어떻게 생겼는가?” 관계는 양의 관계(지느러미 길이가 길어질수록 몸무게도 증가함)인 것으로 보이고, 상당히 선형적이며(점들이 곡선보다는 직선 주변에 모여 있음), 중간 정도의 강도를 보입니다(직선 주변에 흩어진 정도가 아주 크지는 않음). 지느러미가 더 긴 펭귄들이 일반적으로 몸무게 측면에서 더 큽니다.

여기서 짚고 넘어갈 점은 penguins 데이터 프레임의 모든 것을 플롯했지만, 정의되지 않은 값(undefined values)을 가진 행이 몇 개 있었고, 당연히 이들은 플롯될 수 없었다는 것입니다.

에스테틱과 레이어 추가하기

산점도는 두 수치형 변수 사이의 관계를 표시하는 데 유용하지만, 두 변수 사이의 겉으로 보이는 관계를 항상 의심해보고 그 관계를 설명하거나 변화시킬 수 있는 다른 변수들이 있는지 물어보는 것이 좋습니다. 예를 들어 지느러미 길이와 몸무게 사이의 관계가 종에 따라 다를까요?

그래프에 종(species)을 포함시켜 이 변수들 사이의 겉으로 보이는 관계에 대한 추가적인 통찰력을 얻을 수 있는지 확인해 보겠습니다. 우리는 종을 서로 다른 색상의 점으로 표현함으로써 이를 수행할 것입니다.

이를 위해 에스테틱과 geom 중 어느 것을 수정해야 할까요? 만약 “aes() 내부의 에스테틱 매핑”이라고 추측하셨다면, 이미 letsplot으로 데이터 시각화를 만드는 법을 익히기 시작하신 겁니다! 그렇지 않더라도 걱정하지 마세요.

책 전체를 통해 더 많은 그래프를 만들 것이고, 그때마다 여러분의 직관을 확인할 수 있는 기회가 많을 것입니다.

코드 보기
(
    ggplot(
        data=penguins,
        mapping=aes(x="flipper_length_mm", y="body_mass_g", color="species"),
    )
    + geom_point()
)

범주형 변수가 에스테틱에 매핑되면, letsplot은 자동으로 해당 에스테틱의 고유한 값(여기서는 고유한 색상)을 변수의 각 고유 레벨(세 가지 종 각각)에 할당하며, 이 과정을 스케일링(scaling)이라고 합니다.

letsplot은 또한 어떤 값이 어떤 레벨에 해당하는지 설명하는 범례(legend)를 추가합니다.

이제 레이어를 하나 더 추가해 보겠습니다: 몸무게와 지느러미 길이 사이의 관계를 보여주는 매끄러운 곡선입니다.

진행하기 전에 위의 코드를 다시 보고, 기존 그래프에 이를 어떻게 추가할 수 있을지 생각해 보십시오.

이것은 데이터를 표현하는 새로운 기하학적 객체이므로, 포인트 geom 위에 새로운 geom 레이어를 추가할 것입니다: geom_smooth().

그리고 method = "lm"을 사용하여 linear model(선형 모델)을 기반으로 한 최적 적합선(line of best fit)을 그리도록 지정할 것입니다.

코드 보기
(
    ggplot(
        data=penguins,
        mapping=aes(x="flipper_length_mm", y="body_mass_g", color="species"),
    )
    + geom_point()
    + geom_smooth(method="lm")
)

선을 성공적으로 추가했지만, 이 그래프는 앞서 본 그래프와 다릅니다. 앞서 본 그래프는 펭귄 종마다 별도의 선이 있는 것이 아니라 전체 데이터셋에 대해 하나의 선만 있었기 때문입니다.

에스테틱 매핑이 ggplot()에서 전역(global) 레벨로 정의되면, 이후의 모든 geom 레이어로 전달됩니다.

하지만 letplot의 각 geom 함수는 mapping 인수를 가질 수 있으며, 이를 통해 전역 레벨에서 상속받은 것에 추가되는 지역(local) 레벨의 에스테틱 매핑이 가능합니다.

우리는 점들이 종에 따라 색칠되기를 원하지만 선들이 종별로 분리되기를 원하지 않으므로, color = speciesgeom_point()에만 지정해야 합니다. 따라서 전역 aes()에서 이를 빼고 geom_point()에만 추가합니다.

코드 보기
(
    ggplot(data=penguins, mapping=aes(x="flipper_length_mm", y="body_mass_g"))
    + geom_point(mapping=aes(color="species"))
    + geom_smooth(method="lm")
)

짜잔! 아직 완벽하지는 않지만 우리의 최종 목표와 매우 흡사한 모습이 되었습니다.

아직 펭귄 종마다 서로 다른 모양(shape)을 사용해야 하고 레이블도 개선해야 합니다.

사람마다 색맹이나 다른 색각 차이로 인해 색을 다르게 인지하기 때문에, 그래프에서 색상만으로 정보를 표현하는 것은 일반적으로 좋은 생각이 아닙니다. 따라서 색상 외에도 speciesshape 에스테틱에 매핑할 수 있습니다.

코드 보기
(
    ggplot(data=penguins, mapping=aes(x="flipper_length_mm", y="body_mass_g"))
    + geom_point(mapping=aes(color="species", shape="species"))
    + geom_smooth(method="lm")
)

범례도 점들의 서로 다른 모양을 반영하도록 자동으로 업데이트되는 것을 확인하세요.

마지막으로 새로운 레이어에서 labs() 함수를 사용하여 그래프의 레이블을 개선할 수 있습니다. labs()의 인수 중 일부는 직관적일 것입니다: title은 제목을 추가하고 subtitle은 부제목을 추가합니다. 다른 인수들은 에스테틱 매핑과 일치합니다. x는 x축 레이블, y는 y축 레이블이며, colorshape은 범례의 레이블을 정의합니다.

코드 보기
(
    ggplot(data=penguins, mapping=aes(x="flipper_length_mm", y="body_mass_g"))
    + geom_point(aes(color="species", shape="species"))
    + geom_smooth(method="lm")
    + labs(
        title="몸무게와 지느러미 길이",
        subtitle="아델리, 턱끈, 젠투 펭귄의 치수",
        x="지느러미 길이 (mm)",
        y="몸무게 (g)",
        color="종",
        shape="종",
    )
)

드디어 우리의 “궁극적인 목표”와 완벽하게 일치하는 그래프를 얻었습니다!

연습 문제

  1. penguins에는 몇 개의 행이 있나요? 몇 개의 열이 있나요?

  2. penguins 데이터 프레임의 bill_depth_mm 변수는 무엇을 설명하나요? load_penguins()의 도움말을 읽어 확인해 보세요(예: help(load_penguins)).

  3. bill_depth_mmbill_length_mm 산점도를 만드세요. 즉, y축에 bill_depth_mm를, x축에 bill_length_mm를 둔 산점도를 만드세요. 이 두 변수 사이의 관계를 설명해 보세요.

  4. speciesbill_depth_mm 산점도를 만들면 어떻게 되나요? 더 나은 geom 선택은 무엇일까요?

  5. 다음 코드가 에러를 발생시키는 이유는 무엇이며 어떻게 고칠 수 있을까요?

    (ggplot(data = penguins) + 
      geom_point())
  6. 이전 연습 문제에서 만든 그래프에 다음 캡션을 추가하세요: “Data come from the palmerpenguins package.” 힌트: labs()의 문서를 살펴보세요.

  7. 다음 시각화를 재현해 보세요. bill_depth_mm는 어떤 에스테틱에 매핑되어야 할까요? 전역 레벨과 geom 레벨 중 어디에 매핑되어야 할까요?

코드 보기
(
    ggplot(data=penguins, mapping=aes(x="flipper_length_mm", y="body_mass_g"))
    + geom_point(aes(color="bill_depth_mm"))
    + geom_smooth()
)
  1. 이 코드를 머릿속으로 실행해 보고 출력이 어떻게 생겼을지 예측해 보세요. 그런 다음 파이썬에서 코드를 실행하고 예측을 확인해 보세요.

    
    (ggplot(
      data = penguins,
      mapping = aes(x = "flipper_length_mm", y = "body_mass_g", color = "island")
    ) +
      geom_point() +
      geom_smooth(se = False)
    )
  2. 이 두 그래프는 다르게 보일까요? 왜 그럴까요/그렇지 않을까요?

    
    (ggplot(
      data = penguins,
      mapping = aes(x = "flipper_length_mm", y = "body_mass_g")
    ) +
      geom_point() +
      geom_smooth()
    )
    (ggplot() +
      geom_point(
        data = penguins,
        mapping = aes(x = "flipper_length_mm", y = "body_mass_g")
      ) +
      geom_smooth(
        data = penguins,
        mapping = aes(x = "flipper_length_mm", y = "body_mass_g")
      )
    )

letsplot 호출

소개 섹션을 마무리하면서, 이제 letsplot 코드를 더 간결하게 표현하는 방식으로 전환하겠습니다.

지금까지는 배울 때 도움이 되도록 매우 명시적으로 작성했습니다:

(ggplot(
  data = penguins,
  mapping = aes(x = "flipper_length_mm", y = "body_mass_g")
) +
  geom_point())

일반적으로 함수의 첫 번째나 두 번째 인수는 매우 중요하므로 암기해 두는 것이 좋습니다. ggplot()의 처음 두 인수는 datamapping입니다. 책의 나머지 부분에서는 이러한 이름을 일일이 적지 않을 것입니다. 함수가 작성된 방식에 따라 파이썬은 위치를 보고 이러한 변수들을 기대한다는 것을 알기 때문입니다. 이름을 쓰지 않으면 타이핑을 줄일 수 있고, 불필요한 텍스트를 줄임으로써 그래프 간의 차이점을 더 쉽게 확인할 수 있습니다. 이는 나중에 다시 다룰 정말 중요한 프로그래밍 고려 사항입니다.

이전 그래프를 더 간결하게 다시 작성하면 다음과 같습니다:

(
    ggplot(penguins, aes(x = "flipper_length_mm", y = "body_mass_g")) + 
  geom_point()
)

분포 시각화하기

변수의 분포를 시각화하는 방법은 변수의 타입(범주형 또는 수치형)에 따라 다릅니다.

범주형 변수

변수가 적은 수의 값 중 하나만 가질 수 있다면 범주형(categorical)입니다. 범주형 변수의 분포를 조사하려면 막대 그래프(bar chart)를 사용할 수 있습니다. 막대의 높이는 각 x 값에 대해 얼마나 많은 관측치가 나타났는지를 보여줍니다.

코드 보기
(ggplot(penguins, aes(x="species")) + geom_bar())

앞서 "species" 열의 데이터 타입이 문자열인 것을 보셨을 것입니다. 이상적으로는 이것이 범주형(categorical)이어야 합니다. 그래야 우리가 한정된 수의 상호 배타적인 그룹을 다루고 있다는 사실에 혼동이 없기 때문입니다. 또 다른 장점은 시각화 도구가 어떤 종류의 데이터를 다루고 있는지 인식할 수 있게 해준다는 점입니다.

pandas를 사용하여 변수를 범주형 변수로 변환할 수 있습니다:

코드 보기
penguins["species"] = penguins["species"].astype("category")
penguins.head()
species island bill_length_mm bill_depth_mm flipper_length_mm body_mass_g sex year
0 Adelie Torgersen 39.1 18.7 181.0 3750.0 male 2007
1 Adelie Torgersen 39.5 17.4 186.0 3800.0 female 2007
2 Adelie Torgersen 40.3 18.0 195.0 3250.0 female 2007
3 Adelie Torgersen NaN NaN NaN NaN NaN 2007
4 Adelie Torgersen 36.7 19.3 193.0 3450.0 female 2007

범주형 변수에 대해서는 책의 뒷부분에서 더 자세히 배우게 됩니다.

수치형 변수

변수가 광범위한 수치 값을 가질 수 있고 그 값들로 더하기, 빼기 또는 평균 내기가 가능하다면 수치형(numerical) (또는 정량적) 변수입니다. 수치형 변수는 연속형(continuous)일 수도 있고 이산형(discrete)일 수도 있습니다.

연속형 변수의 분포에 흔히 사용되는 시각화 방법 중 하나는 히스토그램입니다.

코드 보기
(ggplot(penguins, aes(x="body_mass_g")) + geom_histogram(binwidth=200))

히스토그램은 x축을 균일한 간격의 빈(bins)으로 나누고 막대의 높이를 사용하여 각 빈에 속하는 관측치의 수를 표시합니다. 위 그래프에서 가장 높은 막대는 3,500에서 3,700그램 사이(막대의 왼쪽과 오른쪽 끝)의 body_mass_g 값을 가진 관측치가 39개임을 보여줍니다.

히스토그램에서 간격의 너비는 x 변수의 단위로 측정되는 binwidth 인수로 설정할 수 있습니다. 히스토그램 작업을 할 때는 항상 다양한 binwidth를 시도해 보아야 합니다. 너비에 따라 서로 다른 패턴이 드러날 수 있기 때문입니다. 아래 그래프들 중 binwidth가 20인 경우는 너무 좁아서 막대가 너무 많아 분포의 모양을 결정하기 어렵습니다. 반대로 binwidth가 2,000인 경우는 너무 높아서 모든 데이터가 단 3개의 막대에 몰리게 되어 분포의 모양을 파악하기 어렵습니다. binwidth 200이 적절한 균형을 제공하지만, 특히 히스토그램은 오해의 소지가 있을 수 있으므로 항상 데이터를 여러 방식으로 살펴보아야 합니다.

수치형 변수의 분포를 보여주는 또 다른 시각화 방법은 밀도 플롯(density plot)입니다. 밀도 플롯은 히스토그램을 부드럽게 만든 버전이며, 특히 기저의 매끄러운 분포에서 나온 연속형 데이터에 실용적인 대안입니다. geom_density()가 밀도를 어떻게 추정하는지는 여기서 다루지 않겠지만(함수 문서에서 더 읽어보실 수 있습니다), 밀도 곡선이 어떻게 그려지는지 비유를 통해 설명하겠습니다. 나무 블록으로 만든 히스토그램을 상상해 보십시오. 그 위에 잘 익은 스파게티 가닥을 떨어뜨린다고 상상해 보세요. 스파게티 가닥이 블록 위에 걸쳐지며 형성하는 모양이 밀도 곡선의 모양이라고 생각할 수 있습니다. 히스토그램보다 세부 사항은 적게 보여주지만 최빈값(modes)과 왜도(skewness) 측면에서 분포의 모양을 빠르게 파악하기 더 쉬울 수 있습니다.

코드 보기
(ggplot(penguins, aes(x="body_mass_g")) + geom_density())

연습 문제

  1. penguins"species" 막대 그래프를 만드는데, "species"y 에스테틱에 할당해 보세요. 이 그래프는 어떻게 다른가요?

  2. 다음 두 그래프는 어떻게 다른가요? 막대의 색상을 변경하는 데 colorfill 중 어떤 에스테틱이 더 유용한가요?

    
    (ggplot(penguins, aes(x = species)) +
      geom_bar(color = "red"))
    
    (ggplot(penguins, aes(x = species)) +
      geom_bar(fill = "red"))
  3. geom_histogram()bins 인수는 무엇을 하나요?

관계 시각화하기

관계를 시각화하려면 그래프의 에스테틱에 최소 두 개의 변수가 매핑되어야 합니다. 단, 상관관계가 인과관계를 의미하지 않으며, 인과관계 또한 상관관계를 의미하지 않는다는 점을 항상 기억해야 합니다!

다음 섹션에서는 둘 이상의 변수 간의 관계를 시각화하는 데 흔히 사용되는 그래프들과 이를 만드는 데 사용되는 geom들을 배우게 될 것입니다.

수치형 변수와 범주형 변수

수치형 변수와 범주형 변수 사이의 관계를 시각화하기 위해 나란히 배치된 박스 플롯(side-by-side box plots)을 사용할 수 있습니다.

박스 플롯(boxplot)은 분포의 위치 척도(백분위수)를 시각적으로 나타낸 일종의 약칭입니다.

또한 잠재적인 이상치를 식별하는 데도 유용합니다. 각 박스 플롯은 다음과 같이 구성됩니다:

  • 데이터의 중간 절반 범위를 나타내는 상자. 이는 사분위간 범위(IQR)로 알려진 거리로, 분포의 25번째 백분위수부터 75번째 백분위수까지 뻗어 있습니다. 상자 중앙에는 분포의 중앙값(즉, 50번째 백분위수)을 표시하는 선이 있습니다. 이 세 개의 선은 분포의 퍼짐 정도와 분포가 중앙값을 중심으로 대칭인지 아니면 한쪽으로 치우쳐 있는지를 가늠하게 해줍니다.

  • 상자의 어느 쪽 끝에서든 IQR의 1.5배 이상 떨어진 관측치를 표시하는 시각적 점들. 이러한 이상치들은 특이한 포인트이므로 개별적으로 플롯됩니다.

  • 상자의 각 끝에서 뻗어 나가 분포 내에서 이상치가 아닌 가장 먼 지점까지 연결되는 선(또는 수염, whisker).

geom_boxplot()을 사용하여 종별 몸무게 분포를 살펴보겠습니다:

코드 보기
(ggplot(penguins, aes(x="species", y="body_mass_g")) + geom_boxplot())

대안으로, geom_density()를 사용하여 확률 밀도 플롯을 만들 수도 있습니다.

코드 보기
(ggplot(penguins, aes(x="body_mass_g", color="species")) + geom_density(size=2))

배경에서 선들이 좀 더 잘 보이도록 size 인수를 사용하여 선의 두께를 조절했습니다.

또한 speciescolorfill 에스테틱 모두에 매핑하고 alpha 에스테틱을 사용하여 채워진 밀도 곡선에 투명도를 더할 수 있습니다. 이 에스테틱은 0(완전 투명)과 1(완전 불투명) 사이의 값을 가집니다. 다음 그래프에서는 0.5로 설정되었습니다.

코드 보기
(
    ggplot(penguins, aes(x="body_mass_g", color="species", fill="species"))
    + geom_density(alpha=0.5)
)

여기서 사용한 용어들에 유의하세요:

  • 변수의 값에 따라 에스테틱으로 표현되는 시각적 속성이 달라지기를 원한다면 변수를 에스테틱에 매핑(map)합니다.
  • 그렇지 않은 경우, 에스테틱의 값을 설정(set)합니다.

두 범주형 변수

누적 막대 그래프(stacked bar plots)를 사용하여 두 범주형 변수 간의 관계를 시각화할 수 있습니다.

예를 들어, 다음 두 개의 누적 막대 그래프는 모두 islandspecies 사이의 관계를 보여주며, 구체적으로는 각 섬 내에서의 species 분포를 시각화합니다.

첫 번째 그래프는 각 섬에 있는 각 종 펭귄들의 빈도수(counts)를 보여줍니다. 빈도수 그래프는 아델리 펭귄들이 각 섬에 동일한 수만큼 있다는 것을 보여줍니다.

하지만 각 섬 내부에서의 퍼센트 비중은 잘 파악되지 않습니다.

코드 보기
(ggplot(penguins, aes(x="island", fill="species")) + geom_bar())

geom에서 position = "fill"을 설정하여 만든 두 번째 그래프는 상대 빈도 그래프로, 섬마다 펭귄의 총 숫자가 다른 것에 영향을 받지 않기 때문에 섬 간의 종 분포를 비교하는 데 더 유용합니다.

이 그래프를 통해 젠투 펭귄들은 모두 비스코(Biscoe) 섬에 살고 있으며 해당 섬 펭귄의 약 75%를 차지하고, 턱끈 펭귄들은 모두 드림(Dream) 섬에 살며 해당 섬 펭귄의 약 50%를 차지하며, 아델리 펭귄들은 세 섬 모두에 살고 토거센(Torgersen) 섬 펭귄의 전부를 차지한다는 것을 알 수 있습니다.

코드 보기
(ggplot(penguins, aes(x="island", fill="species")) + geom_bar(position="fill"))

이러한 막대 그래프를 만들 때, 막대로 분리될 변수를 x 에스테틱에 매핑하고, 막대 내부의 색상을 결정할 변수를 fill 에스테틱에 매핑합니다.

두 수치형 변수

지금까지 두 수치형 변수 사이의 관계를 시각화하기 위해 산점도(geom_point()로 생성)와 매끄러운 곡선(geom_smooth()로 생성)에 대해 배웠습니다. 산점도는 아마도 두 수치형 변수 사이의 관계를 시각화하는 데 가장 흔히 사용되는 그래프일 것입니다.

코드 보기
(ggplot(penguins, aes(x="flipper_length_mm", y="body_mass_g")) + geom_point())

세 개 이상의 변수

앞서 보았듯이, 변수들을 추가적인 에스테틱에 매핑함으로써 그래프에 더 많은 변수를 포함시킬 수 있습니다.

예를 들어 다음 산점도에서 점의 색상은 종(species)을 나타내고 점의 모양은 섬(island)을 나타냅니다.

코드 보기
(
    ggplot(penguins, aes(x="flipper_length_mm", y="body_mass_g"))
    + geom_point(aes(color="species", shape="island"))
)

하지만 그래프에 너무 많은 에스테틱 매핑을 추가하면 그래프가 복잡해지고 의미를 파악하기 어려워집니다.

범주형 변수에 특히 유용한 또 다른 방법은 그래프를 패싯(facets) (또는 스몰 멀티플, small multiples)로 나누는 것입니다. 이는 각각 데이터의 한 부분 집합을 표시하는 서브플롯들입니다.

단일 변수로 그래프를 패싯하려면 facet_wrap()을 사용하세요.

facet_wrap()의 첫 번째 인수는 연속된 차트들에 어떤 변수를 둘지 함수에 알려줍니다. facet_wrap()에 전달하는 변수는 범주형이어야 합니다.

코드 보기
(
    ggplot(penguins, aes(x="flipper_length_mm", y="body_mass_g"))
    + geom_point(aes(color="species", shape="species"))
    + facet_wrap(facets="island")
)

변수의 분포와 변수 간의 관계를 시각화하기 위한 다른 많은 geom들에 대해서는 이후 장에서 배우게 될 것입니다.

연습 문제

  1. bill_depth_mmbill_length_mm 산점도를 만들고 점들을 species별로 색칠해 보세요. 종별 색상 추가가 이 두 변수 사이의 관계에 대해 무엇을 드러내나요? species별 패싯은 어떤가요?

  2. 다음 코드가 왜 두 개의 별도 범례를 생성하나요? 두 범례를 하나로 합치려면 어떻게 수정해야 할까요?

    (
        ggplot(
          data = penguins,
          mapping = aes(
            x = "bill_length_mm", y = "bill_depth_mm", 
            color = "species", shape = "species"
          )
        ) +
          geom_point() +
          labs(color = "종")
    )
  3. 다음 두 개의 누적 막대 그래프를 만드세요. 첫 번째 그래프로 어떤 질문에 답할 수 있나요? 두 번째 그래프로는 어떤 질문에 답할 수 있나요?

    ggplot(penguins, aes(x = "island", fill = "species")) +
      geom_bar(position = "fill")
    ggplot(penguins, aes(x = "species", fill = "island")) +
      geom_bar(position = "fill")

그래프 저장하기

그래프를 만들고 나면 다른 곳에서 사용할 수 있도록 이미지로 저장하고 싶을 것입니다. 가장 최근에 생성된 플롯을 디스크에 저장하는 ggsave()가 그 역할을 합니다:

코드 보기
plotted_data = (
    ggplot(penguins, aes(x="flipper_length_mm", y="body_mass_g")) + geom_point()
)
ggsave(plotted_data, filename="penguin-plot.svg")
'/home/runner/work/python4DS/python4DS/mybook/lets-plot-images/penguin-plot.svg'

이렇게 하면 지정된 위치에 피겨가 저장됩니다. 기본적으로 “lets-plot-images”라는 하위 디렉토리에 저장됩니다.

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

래스터 포맷을 사용하는 경우 scale 키워드 인수를 통해 피겨의 크기를 지정해야 합니다.

연습 문제

  1. 위의 피겨를 PNG로 저장해 보세요. 스케일(scale)을 다양하게 바꿔보세요.

흔한 문제들

코드를 실행하기 시작하면 문제에 부딪힐 가능성이 큽니다. 걱정하지 마세요. 누구에게나 일어나는 일입니다. 우리는 모두 수년간 파이썬 코드를 작성해 왔지만, 여전히 매일 첫 시도에 작동하지 않는 코드를 씁니다!

실행 중인 코드를 책에 있는 코드와 주의 깊게 비교하는 것부터 시작하세요: 문자 하나만 잘못 놓여도 큰 차이가 생길 수 있습니다! 모든 ()와 짝이 맞는지, 모든 "가 다른 "와 쌍을 이루는지 확인하세요. Visual Studio Code에서는 괄호의 색상을 맞춰주는 익스텐션을 설치하여 괄호가 닫혔는지 여부를 쉽게 확인할 수 있습니다.

가끔 코드를 실행해도 아무 일도 일어나지 않을 때가 있습니다.

R 통계 프로그래밍 언어를 사용해 본 분들이라면 + 기호를 잘못된 위치에 놓는 것을 걱정하실 수도 있습니다. 하지만 걱정하지 마세요. letsplot 구문에서는 + 기호가 줄의 시작이나 끝 어디에든 올 수 있기 때문입니다.

여전히 막막하다면 도움말을 활용해 보세요. 대화형 창에서 help(함수_이름)를 실행하여 모든 파이썬 함수에 대한 도움말을 얻을 수 있습니다. 도움말이 별로 도움이 되지 않는 것 같아도 걱정하지 마세요. 대신 예제 부분으로 건너뛰어 여러분이 시도하려는 것과 일치하는 코드를 찾아보세요.

그래도 해결되지 않는다면 letsplot 문서를 확인하거나 구글 검색을 해보세요(에러 메시지 검색이 특히 도움이 됩니다).

요약

이 장에서는 letsplot을 사용한 데이터 시각화의 기초를 배웠습니다. 우리는 letsplot을 지탱하는 기본 아이디어인 시각화는 데이터의 변수들을 위치, 색상, 크기, 모양과 같은 에스테틱 속성에 매핑하는 것이라는 점부터 시작했습니다. 그런 다음 레이어별로 그래프의 복잡성을 높이고 표현을 개선하는 방법을 배웠습니다. 또한 추가적인 에스테틱 매핑을 활용하거나 패싯을 사용하여 그래프를 소규모 다중 그래프로 나눔으로써, 단일 변수의 분포뿐만 아니라 둘 이상의 변수 간의 관계를 시각화하는 데 흔히 사용되는 그래프들에 대해서도 배웠습니다.

우리는 이 책 전반에 걸쳐 시각화를 반복해서 사용할 것이며, 필요에 따라 새로운 기법들을 소개하고 이후 장에서 letsplot을 사용한 시각화 생성에 대해 더 깊이 다룰 것입니다.

시각화의 기초를 다졌으니, 다음 장에서는 주제를 조금 바꾸어 실용적인 워크플로우 조언을 드리겠습니다. 더 많은 파이썬 코드를 작성함에 따라 체계적인 상태를 유지하는 데 도움이 되도록 이 책의 이 부분 전체에 걸쳐 워크플로우 조언과 데이터 과학 도구들을 섞어서 배치했습니다.

Horst, Allison Marie, Alison Presmanes Hill, and Kristen B Gorman. 2020. “Palmerpenguins: Palmer Archipelago (Antarctica) Penguin Data.” R Package Version 0.1.0.