탐색적 데이터 분석 (Exploratory Data Analysis)

서론

이 장에서는 데이터 과학자들이 탐색적 데이터 분석(EDA)이라고 부르는 작업을 체계적으로 수행하기 위해 시각화와 변환을 사용하는 방법을 설명합니다. EDA는 반복적인 사이클로 이루어집니다:

  1. 데이터에 대한 질문을 생성합니다.

  2. 데이터를 시각화, 변환 및 모델링하여 답을 찾습니다.

  3. 배운 내용을 바탕으로 질문을 다듬거나 새로운 질문을 생성합니다.

EDA는 엄격한 규칙이 있는 공식적인 프로세스가 아닙니다. 초기 단계에서는 떠오르는 모든 아이디어를 자유롭게 조사해야 합니다. 그중 일부는 성과를 내고 일부는 막다른 골목에 다다를 것입니다. 탐색이 진행됨에 따라 결국 다른 사람들에게 전달할 수 있는 특히 생산적인 몇 가지 영역으로 좁혀지게 됩니다. 데이터를 탐색할 때는 몇 가지 함정이 있음을 기억해야 합니다. 데이터가 어떻게 수집되었는지, 무엇이 누락되었을지, 품질 문제는 없는지 항상 생각해야 하며 상관관계와 인과관계의 차이에 대해 매우 엄격해야 합니다(이 자체로도 거대한 주제입니다!).

사전 준비

EDA를 위해 pandas, skimpy, pandas-profiling 패키지를 사용할 것입니다. 또한 데이터 시각화를 위해 lets-plot이 필요합니다. 이 모든 것들은 pixi add <packagename>을 통해 설치할 수 있습니다.

항상 그렇듯이, 사용할 패키지들을 불러오는 것으로 시작합니다:

코드 보기
import pandas as pd
from lets_plot import *
from lets_plot.mapping import as_discrete
from pandas.api.types import CategoricalDtype
from skimpy import skim

LetsPlot.setup_html()

질문 (Questions)

“일상적인 통계 질문이란 존재하지 않으며, 오직 의심스러운 통계적 루틴만이 존재할 뿐이다.” — 데이비드 콕스 경 (Sir David Cox)

“틀린 질문에 대한 정확한 답(항상 정밀할 수 있음)보다는, 종종 모호할 수 있지만 옳은 질문에 대한 대략적인 답이 훨씬 낫다.” — 존 터키 (John Tukey)

EDA 기간 동안의 목표는 데이터에 대한 이해를 높이는 것입니다. 이를 수행하는 가장 쉬운 방법은 조사를 안내하는 도구로서 질문을 사용하는 것입니다. 질문을 던지면 그 질문이 데이터셋의 특정 부분에 주의를 집중시키고 어떤 그래프, 모델 또는 변환을 만들어야 할지 결정하는 데 도움을 줍니다.

EDA는 근본적으로 창의적인 프로세스입니다. 그리고 대부분의 창의적인 프로세스와 마찬가지로, 질 좋은 질문을 던지는 핵심은 많은 양의 질문을 생성하는 것입니다. 분석 초기에는 데이터셋에 어떤 통찰력이 담겨 있는지 알지 못하기 때문에 의미 있는 질문을 던지기가 어렵습니다. 반면, 새로운 질문을 던질 때마다 데이터의 새로운 측면에 노출되고 발견을 할 기회가 늘어납니다. 발견한 내용을 바탕으로 꼬리에 꼬리를 무는 질문을 이어 나간다면 데이터의 가장 흥미로운 부분까지 빠르게 파고들어 생각할 거리를 던져주는 질문 세트를 개발할 수 있습니다.

연구를 안내하기 위해 어떤 질문을 해야 한다는 정해진 규칙은 없습니다. 하지만 다음 두 종류의 질문은 데이터 속에서 발견을 하는 데 항상 유용합니다.

  1. 변수 내에서 어떤 유형의 변동(variation)이 발생하는가?

  2. 변수들 사이에서 어떤 유형의 공변동(covariation)이 발생하는가?

이 장의 나머지 부분에서는 이 두 가지 질문을 살펴보겠습니다. 변동과 공변동이 무엇인지 설명하고, 각 질문에 답하는 여러 가지 방법을 보여드리겠습니다.

변동 (Variation)

변동은 변수의 값이 측정 시마다 변하는 경향입니다. 실생활에서도 변동을 쉽게 볼 수 있습니다. 어떤 연속형 변수를 두 번 측정하면 서로 다른 결과를 얻게 될 것입니다. 이는 장비의 미비함 때문에 빛의 속도와 같이 일정한 양을 측정할 때도 마찬가지입니다. 각 측정값에는 측정 시마다 달라지는 작은 오류가 포함됩니다. 변동은 서로 다른 피험자를 측정하거나(예: 사람들의 눈동자 색상) 서로 다른 시점(예: 서로 다른 순간의 전자 에너지 레벨)을 측정할 때도 나타날 수 있습니다. 그 패턴을 이해하는 가장 좋은 방법은 변수 값의 분포를 시각화하는 것입니다.

diamonds 데이터셋에 있는 약 54,000개 다이아몬드의 무게("carat") 분포를 시각화하는 것으로 탐색을 시작하겠습니다.

코드 보기
diamonds = pd.read_csv(
    "https://github.com/mwaskom/seaborn-data/raw/master/diamonds.csv"
)
diamonds["cut"] = diamonds["cut"].astype(
    CategoricalDtype(
        categories=["Fair", "Good", "Very Good", "Premium", "Ideal"], ordered=True
    )
)
diamonds["color"] = diamonds["color"].astype(
    CategoricalDtype(categories=["D", "E", "F", "G", "H", "I", "J"], ordered=True)
)
diamonds.head()
carat cut color clarity depth table price x y z
0 0.23 Ideal E SI2 61.5 55.0 326 3.95 3.98 2.43
1 0.21 Premium E SI1 59.8 61.0 326 3.89 3.84 2.31
2 0.23 Good E VS1 56.9 65.0 327 4.05 4.07 2.31
3 0.29 Premium I VS2 62.4 58.0 334 4.20 4.23 2.63
4 0.31 Good J SI2 63.3 58.0 335 4.34 4.35 2.75

"carat"은 수치형 변수이므로 히스토그램을 사용할 수 있습니다:

코드 보기
(ggplot(diamonds, aes(x="carat")) + geom_histogram(binwidth=0.5))

이제 변동을 시각화할 수 있으므로, 플롯에서 무엇을 찾아야 할까요? 그리고 어떤 유형의 후속 질문을 던져야 할까요? 그래프에서 찾을 수 있는 가장 유용한 정보 유형들과 각 정보 유형에 대한 후속 질문 목록을 아래에 정리했습니다. 좋은 후속 질문을 던지는 핵심은 여러분의 호기심(무엇을 더 배우고 싶은가?)뿐만 아니라 회의론(어떻게 오해의 소지가 있을 수 있는가?)에 의존하는 것입니다.

전형적인 값 (Typical values)

막대 그래프와 히스토그램 모두에서 높은 막대는 변수의 흔한 값을 보여주고 짧은 막대는 덜 흔한 값을 보여줍니다. 막대가 없는 곳은 데이터에서 관찰되지 않은 값을 나타냅니다. 이러한 정보를 유용한 질문으로 바꾸려면 예상치 못한 부분을 찾아보세요:

  • 어떤 값이 가장 흔한가요? 왜 그럴까요?

  • 드문 값은 무엇인가요? 왜 그럴까요? 그것이 여러분의 기대와 일치하나요?

  • 특이한 패턴을 볼 수 있나요? 무엇이 그것을 설명할 수 있을까요?

작은 다이아몬드들의 "carat" 분포를 살펴보겠습니다.

smaller_diamonds를 생성할 때 복사본(copy())을 만들고 있음에 유의하세요. 그렇지 않으면 smaller_diamonds에 가하는 모든 변경 사항이 diamonds에도 영향을 미칩니다(둘은 컴퓨터 메모리상에서 동일한 기저 데이터를 가리킵니다). 때로는 원본 데이터셋에 연결된 상태를 유지하고 싶을 수도 있고 그렇지 않을 수도 있는데, 여기서는 서로 구분되기를 원하므로 copy()를 사용합니다.

코드 보기
smaller_diamonds = diamonds.query("carat < 3").copy()

(ggplot(smaller_diamonds, aes(x="carat")) + geom_histogram(binwidth=0.01))

이 히스토그램은 몇 가지 흥미로운 질문을 던집니다:

  • 왜 정수 캐럿과 흔한 분수 캐럿에 더 많은 다이아몬드가 있을까요?

  • 왜 각 피크의 왼쪽보다 오른쪽 약간 옆에 더 많은 다이아몬드가 있을까요?

시각화는 데이터에 하위 그룹이 존재함을 암시하는 클러스터를 드러낼 수도 있습니다. 하위 그룹을 이해하려면 다음과 같이 물어보세요:

  • 각 하위 그룹 내의 관측치들이 서로 어떻게 유사한가요?

  • 별도의 클러스터에 있는 관측치들은 서로 어떻게 다른가요?

  • 클러스터를 어떻게 설명하거나 묘사할 수 있나요?

  • 왜 클러스터의 모습이 오해의 소지가 있을 수 있나요?

이러한 질문 중 일부는 데이터로 답할 수 있지만 일부는 데이터에 대한 도메인 전문 지식이 필요할 것입니다. 그중 다수는 여러분이 변수들 사이의 관계를 탐색하도록 유도할 것입니다. 예를 들어 한 변수의 값이 다른 변수의 행동을 설명할 수 있는지 확인하는 것입니다. 이에 대해서는 곧 다루겠습니다.

특이한 값 (Unusual values)

이상치(Outliers)는 일반적인 패턴에 맞지 않는 특이한 관측치들입니다. 때로 이상치는 데이터 입력 오류일 수도 있고, 단순히 이 데이터 수집 과정에서 관찰된 극단적인 값일 수도 있으며, 또 다른 때는 중요한 새로운 발견을 암시하기도 합니다. 데이터가 많을 때 이상치는 히스토그램에서 보기 어려울 수 있습니다. 예를 들어 다이아몬드 데이터셋의 "y" 변수 분포를 보십시오. 이상치의 유일한 증거는 x축의 유난히 넓은 제한 범위입니다.

코드 보기
(ggplot(diamonds, aes(x="y")) + geom_histogram(binwidth=0.5))

일반적인 빈(bins)에 너무 많은 관측치가 있어서 드문 빈들은 매우 짧아 보기가 매우 어렵습니다(물론 0 부분을 유심히 쳐다보면 뭔가를 발견할 수도 있겠지만요). 특이한 값들을 쉽게 보려면 coord_cartesian()을 사용하여 y축의 작은 값들로 줌(zoom)을 해야 합니다:

코드 보기
(
    ggplot(diamonds, aes(x="y"))
    + geom_histogram(binwidth=0.5)
    + coord_cartesian(ylim=[0, 50])
)

coord_cartesian()에는 x축을 줌 해야 할 때를 위한 xlim() 인수도 있습니다. Lets-Plot에도 xlim()ylim() 함수가 있지만 이는 약간 다르게 작동합니다. 이들은 제한 범위 밖의 데이터를 버립니다.

이를 통해 0, ~30, ~60 부근에 세 개의 특이한 값이 있음을 볼 수 있습니다. pandas로 이들을 뽑아보겠습니다:

코드 보기
unusual = diamonds.query("y < 3 or y > 20").loc[:, ["x", "y", "z", "price"]]
unusual
x y z price
11963 0.00 0.0 0.00 5139
15951 0.00 0.0 0.00 6381
24067 8.09 58.9 8.06 12210
24520 0.00 0.0 0.00 12800
26243 0.00 0.0 0.00 15686
27429 0.00 0.0 0.00 18034
49189 5.15 31.8 5.12 2075
49556 0.00 0.0 0.00 2130
49557 0.00 0.0 0.00 2130

"y" 변수는 다이아몬드의 세 가지 치수 중 하나를 mm 단위로 측정한 것입니다. 다이아몬드의 너비가 0mm일 수 없으므로 이 값들은 잘못된 것임을 알 수 있습니다. EDA를 수행함으로써 우리는 단순히 NA를 검색해서는 결코 찾을 수 없었을, 0으로 코딩된 누락된 데이터를 발견했습니다. 앞으로 오해의 소지가 있는 계산을 방지하기 위해 이 값들을 NA로 다시 코딩하도록 선택할 수 있습니다. 또한 32mm와 59mm라는 측정값도 믿기 어렵습니다. 그런 다이아몬드들은 길이가 1인치가 넘지만 수십만 달러에 팔리지 않으니까요!

이상치를 포함한 분석과 제외한 분석을 반복해 보는 것이 좋은 습관입니다. 결과에 미치는 영향이 미미하고 왜 거기에 있는지 알 수 없다면 생략하고 넘어가도 괜찮습니다. 하지만 결과에 실질적인 영향을 미친다면 정당한 사유 없이 삭제해서는 안 됩니다. 무엇이 이상치를 유발했는지(예: 데이터 입력 오류) 파악하고 보고서에 이를 제거했음을 밝혀야 합니다.

연습 문제

  1. diamondsx, y, z 각 변수의 분포를 탐색해 보세요. 무엇을 배웠나요? 다이아몬드를 보고 어떤 치수가 길이, 너비, 깊이인지 어떻게 결정할 수 있을지 생각해 보세요.

  2. "price"의 분포를 탐색해 보세요. 특이하거나 놀라운 점을 발견했나요? (힌트: binwidth= 키워드 인수 설정을 신중하게 생각하고 넓은 범위의 값을 시도해 보세요.)

  3. 0.99캐럿 다이아몬드는 몇 개인가요? 1캐럿은 몇 개인가요? 그 차이의 원인이 무엇이라고 생각하시나요?

  4. 히스토그램을 줌 할 때 coord_cartesian()xlim() 또는 ylim()을 비교하고 대조해 보세요. binwidth를 설정하지 않으면 어떻게 되나요? 막대의 절반만 보이도록 줌 하려고 하면 어떻게 되나요?

이상치 처리하기

데이터셋에서 이상치를 발견했고 단순히 분석의 나머지 단계를 진행하고 싶다면 두 가지 옵션이 있습니다.

  1. 이상한 값이 포함된 행 전체를 삭제합니다:

    condition = ((diamonds["y"] < 3) | (diamonds["y"] > 20))
    diamonds2 = diamonds.loc[~condition, :]

    이 옵션은 권장하지 않습니다. 하나의 유효하지 않은 값이 해당 관측치의 다른 모든 값들도 유효하지 않음을 의미하지는 않기 때문입니다. 또한 저품질 데이터를 가지고 있다면 모든 변수에 이 방식을 적용했을 때 남는 데이터가 하나도 없음을 발견하게 될 수도 있습니다!

  2. 대신 특이한 값들을 누락된 값으로 바꾸는 것을 권장합니다. 이상한 값이 교체된 데이터 프레임과 원본 데이터를 구분하는 한 가지 방법은 복사본을 만든 다음 문제의 값을 pandas의 특별한 NA 값인 pd.NA로 설정하는 것입니다.

코드 보기
diamonds2 = diamonds.copy()
condition = (diamonds2["y"] < 3) | (diamonds2["y"] > 20)
diamonds2.loc[condition, "y"] = pd.NA

누락된 값을 어디에 플롯해야 할지 명확하지 않으므로, lets-plot은 이를 플롯에 포함하지 않습니다:

코드 보기
(ggplot(diamonds2, aes(x="x", y="y")) + geom_point())

때로는 누락된 값이 있는 관측치가 기록된 값이 있는 관측치와 어떻게 다른지 이해하고 싶을 때가 있습니다. 예를 들어 nycflights13 데이터에서 "dep_time" 변수의 누락된 값은 항공편이 취소되었음을 나타냅니다. 따라서 취소된 항공편과 취소되지 않은 항공편의 예정된 출발 시간을 비교하고 싶을 수도 있습니다. is.na()를 사용하여 "dep_time"이 누락되었는지 확인하여 새로운 변수를 만들어 이를 수행할 수 있습니다.

코드 보기
url = "https://raw.githubusercontent.com/byuidatascience/data4python4ds/master/data-raw/flights/flights.csv"
flights = pd.read_csv(url)
flights.head()
year month day dep_time sched_dep_time dep_delay arr_time sched_arr_time arr_delay carrier flight tailnum origin dest air_time distance hour minute time_hour
0 2013 1 1 517.0 515 2.0 830.0 819 11.0 UA 1545 N14228 EWR IAH 227.0 1400 5 15 2013-01-01T10:00:00Z
1 2013 1 1 533.0 529 4.0 850.0 830 20.0 UA 1714 N24211 LGA IAH 227.0 1416 5 29 2013-01-01T10:00:00Z
2 2013 1 1 542.0 540 2.0 923.0 850 33.0 AA 1141 N619AA JFK MIA 160.0 1089 5 40 2013-01-01T10:00:00Z
3 2013 1 1 544.0 545 -1.0 1004.0 1022 -18.0 B6 725 N804JB JFK BQN 183.0 1576 5 45 2013-01-01T10:00:00Z
4 2013 1 1 554.0 600 -6.0 812.0 837 -25.0 DL 461 N668DN LGA ATL 116.0 762 6 0 2013-01-01T11:00:00Z
코드 보기
flights2 = flights.assign(
    cancelled=lambda x: pd.isna(x["dep_time"]),
    sched_hour=lambda x: x["sched_dep_time"] // 100,
    sched_min=lambda x: x["sched_dep_time"] % 100,
    sched_dep_time=lambda x: x["sched_hour"] + x["sched_min"] / 60,
)

(
    ggplot(flights2, aes(x="sched_dep_time"))
    + geom_freqpoly(aes(color="cancelled"), binwidth=1 / 4)
)

하지만 취소되지 않은 항공편이 취소된 항공편보다 훨씬 많기 때문에 이 플롯은 좋지 않습니다. 다음 섹션에서는 이 비교를 개선하기 위한 몇 가지 기법을 살펴보겠습니다.

연습 문제

  1. 히스토그램에서 누락된 값은 어떻게 되나요? 막대 그래프에서 누락된 값은 어떻게 되나요? 히스토그램과 막대 그래프에서 누락된 값을 처리하는 방식에 차이가 있는 이유는 무엇인가요?

  2. 취소 여부에 따라 색칠된 scheduled_dep_time 빈도 플롯을 재현해 보세요. 또한 cancelled 변수로 패싯을 해보세요. 취소되지 않은 항공편이 훨씬 더 많은 효과를 완화하기 위해 패싯 함수의 scales 변수에 대해 다양한 값을 시도해 보세요.

공변동 (Covariation)

변동이 변수 의 거동을 묘사한다면, 공변동은 변수 사이의 거동을 묘사합니다. 공변동은 두 개 이상의 변수 값이 관련된 방식으로 함께 변하는 경향입니다. 공변동을 포착하는 가장 좋은 방법은 두 개 이상의 변수 간의 관계를 시각화하는 것이지만, 공변동이 변수 간의 인과관계를 의미하지는 않는다는 점에 유의하세요.

범주형 변수와 수치형 변수

예를 들어 geom_freqpoly()를 사용하여 다이아몬드의 가격이 품질("cut")에 따라 어떻게 변하는지 살펴보겠습니다:

코드 보기
(
    ggplot(diamonds, aes(x="price"))
    + geom_freqpoly(aes(color="cut"), binwidth=500, linewidth=0.75)
)

geom_freqpoly()의 기본 모양은 여기서 그리 유용하지 않습니다. 전체 빈도수에 의해 결정되는 높이가 컷마다 너무 달라서 분포 모양의 차이를 확인하기 어렵기 때문입니다.

비교를 쉽게 하려면 y축에 표시되는 내용을 바꿔야 합니다. 빈도수 대신 밀도(density)를 표시하겠습니다. 밀도는 각 빈도 다각형 아래의 면적이 1이 되도록 표준화된 빈도수입니다.

코드 보기
(
    ggplot(diamonds, aes(x="price"))
    + geom_density(aes(color="cut", fill="cut"), size=1, alpha=0.2)
)

이 플롯에서 다소 놀라운 사실이 발견되었습니다 - 가장 품질이 낮은 ‘fair’ 다이아몬드의 평균 가격이 가장 높은 것처럼 보입니다! 하지만 이는 밀도 플롯이 해석하기 조금 어렵기 때문일 수도 있습니다 - 이 플롯에는 많은 정보가 담겨 있으니까요.

이 관계를 탐색하기 위한 시각적으로 더 간단한 플롯은 나란히 놓인 박스 플롯을 사용하는 것입니다.

코드 보기
(ggplot(diamonds, aes(x="cut", y="price")) + geom_boxplot())

분포에 대한 정보는 훨씬 적게 보이지만, 박스 플롯은 훨씬 더 콤팩트하므로 더 쉽게 비교할 수 있습니다(그리고 한 플롯에 더 많이 넣을 수 있습니다). 더 좋은 품질의 다이아몬드가 일반적으로 더 저렴하다는 반직관적인 발견을 뒷받침합니다! 연습 문제에서는 그 이유를 파악하는 과제가 주어질 것입니다.

"cut"은 순서가 있는 범주형 변수입니다: fair는 good보다 나쁘고, good은 very good보다 나쁜 식입니다. 많은 범주형 변수들은 이러한 고유한 순서가 없으므로 더 정보가 많은 화면을 만들기 위해 재정렬하고 싶을 수 있습니다. 한 가지 방법은 중앙값에 따라 정렬하는 것이지만 다른 옵션들도 가능합니다.

예를 들어 mpg 데이터셋에서 클래스에 따라 고속도로 연비가 어떻게 변하는지 보고 싶을 수 있습니다:

코드 보기
mpg = pd.read_csv(
    "https://vincentarelbundock.github.io/Rdatasets/csv/ggplot2/mpg.csv", index_col=0
)
mpg["class"] = mpg["class"].astype("category")

(ggplot(mpg, aes(x="class", y="hwy")) + geom_boxplot())

경향을 더 쉽게 보기 위해 "hwy"의 중앙값을 기준으로 클래스를 재정렬할 수 있습니다:

코드 보기
(ggplot(mpg) + geom_boxplot(aes(as_discrete("class", order_by="..middle.."), "hwy")))

변수 이름이 길다면 geom_boxplot()을 90도 회전시키는 것이 더 효과적일 수 있습니다. coord_flip()을 추가하여 이를 수행할 수 있습니다.

코드 보기
(
    ggplot(mpg)
    + geom_boxplot(aes(as_discrete("class", order_by="..middle.."), "hwy"))
    + coord_flip()
)

연습 문제

  1. 배운 내용을 활용하여 취소된 항공편과 취소되지 않은 항공편의 출발 시간 시각화를 개선해 보세요.

  2. EDA를 기반으로 할 때, 다이아몬드 데이터셋에서 다이아몬드 가격을 예측하는 데 가장 중요한 변수는 무엇인가요? 그 변수는 cut과 어떤 상관관계가 있나요? 왜 이 두 관계의 조합이 낮은 품질의 다이아몬드를 더 비싸게 만드는 원인이 되나요?

  3. diamonds 데이터셋의 범주형 변수에 따른 가격을 geom_violin(), 패싯된 geom_histogram(), 색칠된 geom_freqpoly(), 그리고 색칠된 geom_density()를 사용하여 시각화해 보세요. 네 개의 플롯을 비교하고 대조해 보세요. 범주형 변수의 레벨에 기반한 수치형 변수의 분포를 시각화할 때 각 방법의 장단점은 무엇인가요?

  4. 데이터셋이 작다면 geom_jitter()를 사용하여 오버플로팅(overplotting)을 피하고 연속형 변수와 범주형 변수 간의 관계를 더 쉽게 확인하는 것이 유용할 때가 있습니다. ggbeeswarm 패키지는 geom_jitter()와 유사한 여러 메서드를 제공합니다. 그들을 나열하고 각각이 무엇을 하는지 간단히 설명해 보세요.

두 범주형 변수

두 범주형 변수 간의 공변동을 시각화하려면, 이들 범주형 변수의 레벨 조합별 관측치 수를 세어야 합니다. pd.crosstab()을 수행한 후 이를 “녹여(melt)” 깔끔한 형식으로 만들어 이를 수행할 수 있습니다.

코드 보기
ct_cut_color = pd.melt(
    pd.crosstab(diamonds["cut"], diamonds["color"]).reset_index(),
    id_vars=["cut"],
    value_vars=diamonds["color"].unique(),
)

이어 geom_tile()을 사용하여 시각화합니다:

코드 보기
(ggplot(ct_cut_color, aes(x="color", y="cut")) + geom_tile(aes(fill="value")))

연습 문제

  1. 위의 카운트 데이터셋을 어떻게 리스케일링하면 색상(color) 내 컷(cut)의 분포, 또는 컷 내 색상의 분포를 더 명확하게 보여줄 수 있을까요?

  2. 색상이 x 에스테틱에 매핑되고 컷이 fill 에스테틱에 매핑된 분할 막대 그래프(segmented bar chart)에서 어떤 다른 데이터 통찰을 얻을 수 있나요? 각 세그먼트에 해당하는 카운트를 계산해 보세요.

  3. geom_tile()pandas와 함께 사용하여 목적지와 연중 월에 따라 평균 항공편 출발 지연 시간이 어떻게 변하는지 탐색해 보세요. 무엇이 플롯을 읽기 어렵게 만드나요? 어떻게 개선할 수 있을까요?

두 수치형 변수

여러분은 이미 두 수치형 변수 사이의 공변동을 시각화하는 훌륭한 방법 하나를 보았습니다: geom_point()로 산점도를 그리는 것입니다. 점들의 패턴으로 공변동을 확인할 수 있습니다. 예를 들어 다이아몬드의 캐럿 크기와 가격 사이의 양의 상관관계를 볼 수 있습니다: 캐럿이 많을수록 다이아몬드 가격이 높습니다. 관계는 기하급수적입니다.

코드 보기
(ggplot(smaller_diamonds, aes(x="carat", y="price")) + geom_point())

(이 섹션에서는 3캐럿보다 작은 대다수의 다이아몬드에 집중하기 위해 smaller_diamonds 데이터셋을 사용하겠습니다)

산점도는 데이터셋의 크기가 커질수록 덜 유용해집니다. 점들이 겹치기 시작하여(overplot) 균일한 검은색 영역으로 쌓이기 때문입니다. 이로 인해 2차원 공간 전체에 걸친 데이터 밀도의 차이를 판단하기 어려울 뿐만 아니라 추세를 파악하기도 힘들어집니다. 이 문제를 해결하는 한 가지 방법을 이미 보았습니다: alpha 에스테틱을 사용하여 투명도를 추가하는 것입니다.

코드 보기
(ggplot(smaller_diamonds, aes(x="carat", y="price")) + geom_point(alpha=1 / 20))

하지만 매우 큰 데이터셋의 경우 투명도를 사용하는 것도 한계가 있을 수 있습니다. 그런 경우에는 빈스캐터(binscatter) 또는 빈으로 묶인 산점도(binned scatterplot)를 권장합니다. 빈으로 묶인 산점도는 조건부 변수(예제의 "carat")를 동일한 크기의 빈 또는 분위수로 나누고, 각 빈 내에서 종속 변수(예제의 "price")의 조건부 평균을 플롯합니다. 빈 스캐터는 종종 신뢰 구간과 함께 제공되기도 합니다. 파이썬에서 좋은 빈 스캐터 패키지는 binsreg입니다. 하지만 빈 스캐터는 고급 주제이므로 여기서는 다루지 않겠습니다.

pandas에 내장된 EDA 도구들

pandas에는 내장된 훌륭한 EDA 옵션들이 있습니다. 사실 우리는 이미 그중 하나인 df.info()를 보았습니다. 이 함수는 데이터 타입과 메모리 사용량을 보고할 뿐만 아니라 각 열에서 얼마나 많은 관측치가 ‘거짓 같은(falsy)’ 값이 아닌 ‘참 같은(truthy)’ 값인지, 즉 얼마나 많은 관측치가 null이 아닌 값을 가지고 있는지 알려줍니다.

탐색적 테이블과 기술 통계

테이블을 얻기 위해 .info()보다 한 걸음 더 나아가는 방법은 .describe()를 사용하는 것입니다. 실수(floats)를 포함한 혼합 데이터 타입을 가지고 있다면 기본적인 요약 통계량을 보고해 줍니다:

코드 보기
diamonds.describe()
carat depth table price x y z
count 53940.000000 53940.000000 53940.000000 53940.000000 53940.000000 53940.000000 53940.000000
mean 0.797940 61.749405 57.457184 3932.799722 5.731157 5.734526 3.538734
std 0.474011 1.432621 2.234491 3989.439738 1.121761 1.142135 0.705699
min 0.200000 43.000000 43.000000 326.000000 0.000000 0.000000 0.000000
25% 0.400000 61.000000 56.000000 950.000000 4.710000 4.720000 2.910000
50% 0.700000 61.800000 57.000000 2401.000000 5.700000 5.710000 3.530000
75% 1.040000 62.500000 59.000000 5324.250000 6.540000 6.540000 4.040000
max 5.010000 79.000000 95.000000 18823.000000 10.740000 58.900000 31.800000

도움은 되지만 읽기가 참 어렵네요! round() 메서드를 함께 사용하여 이를 개선할 수 있습니다:

코드 보기
sum_table = diamonds.describe().round(1)
sum_table
carat depth table price x y z
count 53940.0 53940.0 53940.0 53940.0 53940.0 53940.0 53940.0
mean 0.8 61.7 57.5 3932.8 5.7 5.7 3.5
std 0.5 1.4 2.2 3989.4 1.1 1.1 0.7
min 0.2 43.0 43.0 326.0 0.0 0.0 0.0
25% 0.4 61.0 56.0 950.0 4.7 4.7 2.9
50% 0.7 61.8 57.0 2401.0 5.7 5.7 3.5
75% 1.0 62.5 59.0 5324.2 6.5 6.5 4.0
max 5.0 79.0 95.0 18823.0 10.7 58.9 31.8

공개된 요약 통계 테이블은 종종 행당 하나의 변수를 나열합니다. 데이터 프레임에 변수가 많다면 describe() 결과가 너무 넓어져서 쉽게 읽기 어려울 수 있습니다. T 속성(또는 transpose() 메서드)을 사용하여 전치시킬 수 있습니다:

코드 보기
sum_table = sum_table.T
sum_table
count mean std min 25% 50% 75% max
carat 53940.0 0.8 0.5 0.2 0.4 0.7 1.0 5.0
depth 53940.0 61.7 1.4 43.0 61.0 61.8 62.5 79.0
table 53940.0 57.5 2.2 43.0 56.0 57.0 59.0 95.0
price 53940.0 3932.8 3989.4 326.0 950.0 2401.0 5324.2 18823.0
x 53940.0 5.7 1.1 0.0 4.7 5.7 6.5 10.7
y 53940.0 5.7 1.1 0.0 4.7 5.7 6.5 58.9
z 53940.0 3.5 0.7 0.0 2.9 3.5 4.0 31.8

물론 이 미리 구축된 테이블에서 제공하는 통계치는 커스텀이 잘 되어 있지 않습니다. 그렇다면 우리가 실제로 원하는 테이블을 얻으려면 어떻게 해야 할까요? 답은 이전 데이터 장들, 특히 데이터 분석 입문 편의 내용을 활용하는 것입니다. 그룹바이(groupbys), 머지(merges), 집계(aggregations) 등을 모두 사용하여 여러분이 원하는 EDA 테이블을 만들어 보세요.

데이터를 탐색하고 있다면 모든 것을 명확하게 읽고 예상에서 벗어나는 것을 빠르게 확인하고 싶을 것입니다. pandas에는 여러분을 돕기 위해 데이터 프레임 스타일을 지정하는 몇 가지 내장 기능이 있습니다. 이러한 스타일은 데이터 프레임을 예를 들어 엑셀로 내보낼 때도 유지됩니다.

다음은 데이터 프레임의 스타일을 지정하는 몇 가지 방법을 강조하는 예제입니다. 여기에는 다음과 같은 기능들이 사용되었습니다: 더 넓은 포맷으로 풀기(unstack), 단위 변경(lambda 함수; 참고로 1e3은 컴퓨터에서 1000의 줄임말입니다), NaN을 눈에 띄지 않는 문자열로 채우기(.fillna('-')), 소수점 이하 숫자 제거(.style.format(precision=0)), 그리고 캡션 추가(.style.set_caption).

코드 보기
(
    diamonds.groupby(["cut", "color"])["price"]
    .mean()
    .unstack()
    .apply(lambda x: x / 1e3)
    .fillna("-")
    .style.format(precision=2)
    .set_caption("판매 가격 (천 단위)")
)
/tmp/ipykernel_5667/3320603047.py:2: FutureWarning: The default of observed=False is deprecated and will be changed to True in a future version of pandas. Pass observed=False to retain current behavior or observed=True to adopt the future default and silence this warning.
  diamonds.groupby(["cut", "color"])["price"]
Table 16.1: 판매 가격 (천 단위)
color D E F G H I J
cut              
Fair 4.29 3.68 3.83 4.24 5.14 4.69 4.98
Good 3.41 3.42 3.50 4.12 4.28 5.08 4.57
Very Good 3.47 3.21 3.78 3.87 4.54 5.26 5.10
Premium 3.63 3.54 4.32 4.50 5.22 5.95 6.29
Ideal 2.63 2.60 3.37 3.72 3.89 4.45 4.92

앞서 본 것보다는 깔끔하지만, 여전히 밋밋한 숫자 테이블입니다. 시선이 바로 꽂히지는 않네요!

이를 해결하기 위해 또 다른 스타일링 기법인 색상 사용을 살펴보겠습니다. 컷(cut)과 색상(color) 사이의 교차 요약표(cross-tabulation)를 만들고 싶다고 가정해 봅시다. 즉, 이 두 필드에 나타나는 항목들의 개수를 카테고리별로 보여주는 것입니다.

교차 요약표를 만들기 위해 내장된 pd.crosstab()을 사용하되, 테이블에 나타나는 값(개수)들이 style.background_gradient()를 사용한 히트맵으로 강조되도록 요청하겠습니다:

코드 보기
pd.crosstab(diamonds["color"], diamonds["cut"]).style.background_gradient(cmap="plasma")
cut Fair Good Very Good Premium Ideal
color          
D 163 662 1513 1603 2834
E 224 933 2400 2337 3903
F 312 909 2164 2331 3826
G 314 871 2299 2924 4884
H 303 702 1824 2360 3115
I 175 522 1204 1428 2093
J 119 307 678 808 896

기본적으로 background_gradient()는 각 숫자를 해당 열의 다른 숫자들과 비교하여 강조합니다. axis=1을 사용하여 행별로 강조하거나, axis=0을 사용하여 전체 테이블 값에 대해 상대적으로 강조할 수 있습니다. 물론 plasma사용 가능한 수많은 컬러맵 중 하나일 뿐입니다!

{.callout-note} 연습 문제 다른 컬러맵을 사용하여 새로운 교차 요약표를 만들어 보세요.

데이터 프레임에 대한 몇 가지 다른 스타일링 팁입니다.

먼저, 순서를 보여주기 위해 막대를 사용하세요:

코드 보기
(
    pd.crosstab(diamonds["color"], diamonds["cut"])
    .style.format(precision=0)
    .bar(color="#d65f5f")
)
cut Fair Good Very Good Premium Ideal
color          
D 163 662 1513 1603 2834
E 224 933 2400 2337 3903
F 312 909 2164 2331 3826
G 314 871 2299 2924 4884
H 303 702 1824 2360 3115
I 175 522 1204 1428 2093
J 119 307 678 808 896

중요한 항목을 보여주기 위해 .hightlight_max() 및 유사한 명령어를 사용하세요:

코드 보기
pd.crosstab(diamonds["color"], diamonds["cut"]).style.highlight_max().format("{:.0f}")
cut Fair Good Very Good Premium Ideal
color          
D 163 662 1513 1603 2834
E 224 933 2400 2337 3903
F 312 909 2164 2331 3826
G 314 871 2299 2924 4884
H 303 702 1824 2360 3115
I 175 522 1204 1428 2093
J 119 307 678 808 896

전체 스타일링 명령어 세트는 여기에서 찾을 수 있습니다.

pandas를 사용한 탐색적 플로팅

pandas에는 데이터를 빠르게 살펴볼 수 있는 내장 플로팅 옵션이 있습니다. 상황에 따라 .plot.* 또는 .plot()을 통해 접근할 수 있습니다. 택시 데이터셋을 사용하여 빠른 .plot()을 만들어 보겠습니다.

코드 보기
taxis = pd.read_csv("https://github.com/mwaskom/seaborn-data/raw/master/taxis.csv")
# turn the pickup time column into a datetime
taxis["pickup"] = pd.to_datetime(taxis["pickup"])
# set some other columns types
taxis = taxis.astype(
    {
        "dropoff": "datetime64[ns]",
        "pickup": "datetime64[ns]",
        "color": "category",
        "payment": "category",
        "pickup_zone": "string",
        "dropoff_zone": "string",
        "pickup_borough": "category",
        "dropoff_borough": "category",
    }
)
taxis.head()
pickup dropoff passengers distance fare tip tolls total color payment pickup_zone dropoff_zone pickup_borough dropoff_borough
0 2019-03-23 20:21:09 2019-03-23 20:27:24 1 1.60 7.0 2.15 0.0 12.95 yellow credit card Lenox Hill West UN/Turtle Bay South Manhattan Manhattan
1 2019-03-04 16:11:55 2019-03-04 16:19:00 1 0.79 5.0 0.00 0.0 9.30 yellow cash Upper West Side South Upper West Side South Manhattan Manhattan
2 2019-03-27 17:53:01 2019-03-27 18:00:25 1 1.37 7.5 2.36 0.0 14.16 yellow credit card Alphabet City West Village Manhattan Manhattan
3 2019-03-10 01:23:59 2019-03-10 01:49:51 1 7.70 27.0 6.15 0.0 36.95 yellow credit card Hudson Sq Yorkville West Manhattan Manhattan
4 2019-03-30 13:27:42 2019-03-30 13:37:14 3 2.16 9.0 1.10 0.0 13.40 yellow credit card Midtown East Yorkville West Manhattan Manhattan
코드 보기
taxis.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 6433 entries, 0 to 6432
Data columns (total 14 columns):
 #   Column           Non-Null Count  Dtype         
---  ------           --------------  -----         
 0   pickup           6433 non-null   datetime64[ns]
 1   dropoff          6433 non-null   datetime64[ns]
 2   passengers       6433 non-null   int64         
 3   distance         6433 non-null   float64       
 4   fare             6433 non-null   float64       
 5   tip              6433 non-null   float64       
 6   tolls            6433 non-null   float64       
 7   total            6433 non-null   float64       
 8   color            6433 non-null   category      
 9   payment          6389 non-null   category      
 10  pickup_zone      6407 non-null   string        
 11  dropoff_zone     6388 non-null   string        
 12  pickup_borough   6407 non-null   category      
 13  dropoff_borough  6388 non-null   category      
dtypes: category(4), datetime64[ns](2), float64(5), int64(1), string(2)
memory usage: 528.5 KB
코드 보기
(
    taxis.set_index("pickup")
    .groupby(pd.Grouper(freq="D"))["total"]
    .mean()
    .plot(
        title="평균 택시 요금",
        xlabel="",
        ylabel="요금 (USD)",
    )
);
/home/runner/work/python4DS/python4DS/.pixi/envs/default/lib/python3.10/site-packages/IPython/core/events.py:82: UserWarning: Glyph 50836 (\N{HANGUL SYLLABLE YO}) missing from font(s) STIXGeneral.
  func(*args, **kwargs)
/home/runner/work/python4DS/python4DS/.pixi/envs/default/lib/python3.10/site-packages/IPython/core/events.py:82: UserWarning: Glyph 44552 (\N{HANGUL SYLLABLE GEUM}) missing from font(s) STIXGeneral.
  func(*args, **kwargs)
/home/runner/work/python4DS/python4DS/.pixi/envs/default/lib/python3.10/site-packages/IPython/core/events.py:82: UserWarning: Glyph 54217 (\N{HANGUL SYLLABLE PYEONG}) missing from font(s) STIXGeneral.
  func(*args, **kwargs)
/home/runner/work/python4DS/python4DS/.pixi/envs/default/lib/python3.10/site-packages/IPython/core/events.py:82: UserWarning: Glyph 44512 (\N{HANGUL SYLLABLE GYUN}) missing from font(s) STIXGeneral.
  func(*args, **kwargs)
/home/runner/work/python4DS/python4DS/.pixi/envs/default/lib/python3.10/site-packages/IPython/core/events.py:82: UserWarning: Glyph 53469 (\N{HANGUL SYLLABLE TAEG}) missing from font(s) STIXGeneral.
  func(*args, **kwargs)
/home/runner/work/python4DS/python4DS/.pixi/envs/default/lib/python3.10/site-packages/IPython/core/events.py:82: UserWarning: Glyph 49884 (\N{HANGUL SYLLABLE SI}) missing from font(s) STIXGeneral.
  func(*args, **kwargs)
/home/runner/work/python4DS/python4DS/.pixi/envs/default/lib/python3.10/site-packages/IPython/core/events.py:82: UserWarning: Glyph 50836 (\N{HANGUL SYLLABLE YO}) missing from font(s) STIXGeneral.
  func(*args, **kwargs)
/home/runner/work/python4DS/python4DS/.pixi/envs/default/lib/python3.10/site-packages/IPython/core/events.py:82: UserWarning: Glyph 44552 (\N{HANGUL SYLLABLE GEUM}) missing from font(s) STIXGeneral.
  func(*args, **kwargs)
/home/runner/work/python4DS/python4DS/.pixi/envs/default/lib/python3.10/site-packages/IPython/core/events.py:82: UserWarning: Glyph 54217 (\N{HANGUL SYLLABLE PYEONG}) missing from font(s) STIXGeneral.
  func(*args, **kwargs)
/home/runner/work/python4DS/python4DS/.pixi/envs/default/lib/python3.10/site-packages/IPython/core/events.py:82: UserWarning: Glyph 44512 (\N{HANGUL SYLLABLE GYUN}) missing from font(s) STIXGeneral.
  func(*args, **kwargs)
/home/runner/work/python4DS/python4DS/.pixi/envs/default/lib/python3.10/site-packages/IPython/core/events.py:82: UserWarning: Glyph 53469 (\N{HANGUL SYLLABLE TAEG}) missing from font(s) STIXGeneral.
  func(*args, **kwargs)
/home/runner/work/python4DS/python4DS/.pixi/envs/default/lib/python3.10/site-packages/IPython/core/events.py:82: UserWarning: Glyph 49884 (\N{HANGUL SYLLABLE SI}) missing from font(s) STIXGeneral.
  func(*args, **kwargs)
/home/runner/work/python4DS/python4DS/.pixi/envs/default/lib/python3.10/site-packages/IPython/core/pylabtools.py:170: UserWarning: Glyph 50836 (\N{HANGUL SYLLABLE YO}) missing from font(s) STIXGeneral.
  fig.canvas.print_figure(bytes_io, **kw)
/home/runner/work/python4DS/python4DS/.pixi/envs/default/lib/python3.10/site-packages/IPython/core/pylabtools.py:170: UserWarning: Glyph 44552 (\N{HANGUL SYLLABLE GEUM}) missing from font(s) STIXGeneral.
  fig.canvas.print_figure(bytes_io, **kw)
/home/runner/work/python4DS/python4DS/.pixi/envs/default/lib/python3.10/site-packages/IPython/core/pylabtools.py:170: UserWarning: Glyph 54217 (\N{HANGUL SYLLABLE PYEONG}) missing from font(s) STIXGeneral.
  fig.canvas.print_figure(bytes_io, **kw)
/home/runner/work/python4DS/python4DS/.pixi/envs/default/lib/python3.10/site-packages/IPython/core/pylabtools.py:170: UserWarning: Glyph 44512 (\N{HANGUL SYLLABLE GYUN}) missing from font(s) STIXGeneral.
  fig.canvas.print_figure(bytes_io, **kw)
/home/runner/work/python4DS/python4DS/.pixi/envs/default/lib/python3.10/site-packages/IPython/core/pylabtools.py:170: UserWarning: Glyph 53469 (\N{HANGUL SYLLABLE TAEG}) missing from font(s) STIXGeneral.
  fig.canvas.print_figure(bytes_io, **kw)
/home/runner/work/python4DS/python4DS/.pixi/envs/default/lib/python3.10/site-packages/IPython/core/pylabtools.py:170: UserWarning: Glyph 49884 (\N{HANGUL SYLLABLE SI}) missing from font(s) STIXGeneral.
  fig.canvas.print_figure(bytes_io, **kw)
/home/runner/work/python4DS/python4DS/.pixi/envs/default/lib/python3.10/site-packages/IPython/core/pylabtools.py:170: UserWarning: Glyph 50836 (\N{HANGUL SYLLABLE YO}) missing from font(s) STIXGeneral.
  fig.canvas.print_figure(bytes_io, **kw)
/home/runner/work/python4DS/python4DS/.pixi/envs/default/lib/python3.10/site-packages/IPython/core/pylabtools.py:170: UserWarning: Glyph 44552 (\N{HANGUL SYLLABLE GEUM}) missing from font(s) STIXGeneral.
  fig.canvas.print_figure(bytes_io, **kw)
/home/runner/work/python4DS/python4DS/.pixi/envs/default/lib/python3.10/site-packages/IPython/core/pylabtools.py:170: UserWarning: Glyph 54217 (\N{HANGUL SYLLABLE PYEONG}) missing from font(s) STIXGeneral.
  fig.canvas.print_figure(bytes_io, **kw)
/home/runner/work/python4DS/python4DS/.pixi/envs/default/lib/python3.10/site-packages/IPython/core/pylabtools.py:170: UserWarning: Glyph 44512 (\N{HANGUL SYLLABLE GYUN}) missing from font(s) STIXGeneral.
  fig.canvas.print_figure(bytes_io, **kw)
/home/runner/work/python4DS/python4DS/.pixi/envs/default/lib/python3.10/site-packages/IPython/core/pylabtools.py:170: UserWarning: Glyph 53469 (\N{HANGUL SYLLABLE TAEG}) missing from font(s) STIXGeneral.
  fig.canvas.print_figure(bytes_io, **kw)
/home/runner/work/python4DS/python4DS/.pixi/envs/default/lib/python3.10/site-packages/IPython/core/pylabtools.py:170: UserWarning: Glyph 49884 (\N{HANGUL SYLLABLE SI}) missing from font(s) STIXGeneral.
  fig.canvas.print_figure(bytes_io, **kw)

다시 말하지만, 데이터를 올바른 모양으로 만들 수 있다면 플롯을 그릴 수 있습니다. 동일한 함수가 여러 선에 대해서도 작동합니다.

코드 보기
(
    taxis.set_index("pickup")
    .groupby(pd.Grouper(freq="D"))[["fare", "tip", "tolls"]]
    .mean()
    .plot(
        style=["-", ":", "-."],
        title="택시 요금 구성 요소",
        xlabel="",
        ylabel="USD",
    )
);
/home/runner/work/python4DS/python4DS/.pixi/envs/default/lib/python3.10/site-packages/IPython/core/events.py:82: UserWarning: Glyph 53469 (\N{HANGUL SYLLABLE TAEG}) missing from font(s) STIXGeneral.
  func(*args, **kwargs)
/home/runner/work/python4DS/python4DS/.pixi/envs/default/lib/python3.10/site-packages/IPython/core/events.py:82: UserWarning: Glyph 49884 (\N{HANGUL SYLLABLE SI}) missing from font(s) STIXGeneral.
  func(*args, **kwargs)
/home/runner/work/python4DS/python4DS/.pixi/envs/default/lib/python3.10/site-packages/IPython/core/events.py:82: UserWarning: Glyph 50836 (\N{HANGUL SYLLABLE YO}) missing from font(s) STIXGeneral.
  func(*args, **kwargs)
/home/runner/work/python4DS/python4DS/.pixi/envs/default/lib/python3.10/site-packages/IPython/core/events.py:82: UserWarning: Glyph 44552 (\N{HANGUL SYLLABLE GEUM}) missing from font(s) STIXGeneral.
  func(*args, **kwargs)
/home/runner/work/python4DS/python4DS/.pixi/envs/default/lib/python3.10/site-packages/IPython/core/events.py:82: UserWarning: Glyph 44396 (\N{HANGUL SYLLABLE GU}) missing from font(s) STIXGeneral.
  func(*args, **kwargs)
/home/runner/work/python4DS/python4DS/.pixi/envs/default/lib/python3.10/site-packages/IPython/core/events.py:82: UserWarning: Glyph 49457 (\N{HANGUL SYLLABLE SEONG}) missing from font(s) STIXGeneral.
  func(*args, **kwargs)
/home/runner/work/python4DS/python4DS/.pixi/envs/default/lib/python3.10/site-packages/IPython/core/events.py:82: UserWarning: Glyph 49548 (\N{HANGUL SYLLABLE SO}) missing from font(s) STIXGeneral.
  func(*args, **kwargs)
/home/runner/work/python4DS/python4DS/.pixi/envs/default/lib/python3.10/site-packages/IPython/core/events.py:82: UserWarning: Glyph 53469 (\N{HANGUL SYLLABLE TAEG}) missing from font(s) STIXGeneral.
  func(*args, **kwargs)
/home/runner/work/python4DS/python4DS/.pixi/envs/default/lib/python3.10/site-packages/IPython/core/events.py:82: UserWarning: Glyph 49884 (\N{HANGUL SYLLABLE SI}) missing from font(s) STIXGeneral.
  func(*args, **kwargs)
/home/runner/work/python4DS/python4DS/.pixi/envs/default/lib/python3.10/site-packages/IPython/core/events.py:82: UserWarning: Glyph 50836 (\N{HANGUL SYLLABLE YO}) missing from font(s) STIXGeneral.
  func(*args, **kwargs)
/home/runner/work/python4DS/python4DS/.pixi/envs/default/lib/python3.10/site-packages/IPython/core/events.py:82: UserWarning: Glyph 44552 (\N{HANGUL SYLLABLE GEUM}) missing from font(s) STIXGeneral.
  func(*args, **kwargs)
/home/runner/work/python4DS/python4DS/.pixi/envs/default/lib/python3.10/site-packages/IPython/core/events.py:82: UserWarning: Glyph 44396 (\N{HANGUL SYLLABLE GU}) missing from font(s) STIXGeneral.
  func(*args, **kwargs)
/home/runner/work/python4DS/python4DS/.pixi/envs/default/lib/python3.10/site-packages/IPython/core/events.py:82: UserWarning: Glyph 49457 (\N{HANGUL SYLLABLE SEONG}) missing from font(s) STIXGeneral.
  func(*args, **kwargs)
/home/runner/work/python4DS/python4DS/.pixi/envs/default/lib/python3.10/site-packages/IPython/core/events.py:82: UserWarning: Glyph 49548 (\N{HANGUL SYLLABLE SO}) missing from font(s) STIXGeneral.
  func(*args, **kwargs)
/home/runner/work/python4DS/python4DS/.pixi/envs/default/lib/python3.10/site-packages/IPython/core/pylabtools.py:170: UserWarning: Glyph 53469 (\N{HANGUL SYLLABLE TAEG}) missing from font(s) STIXGeneral.
  fig.canvas.print_figure(bytes_io, **kw)
/home/runner/work/python4DS/python4DS/.pixi/envs/default/lib/python3.10/site-packages/IPython/core/pylabtools.py:170: UserWarning: Glyph 49884 (\N{HANGUL SYLLABLE SI}) missing from font(s) STIXGeneral.
  fig.canvas.print_figure(bytes_io, **kw)
/home/runner/work/python4DS/python4DS/.pixi/envs/default/lib/python3.10/site-packages/IPython/core/pylabtools.py:170: UserWarning: Glyph 50836 (\N{HANGUL SYLLABLE YO}) missing from font(s) STIXGeneral.
  fig.canvas.print_figure(bytes_io, **kw)
/home/runner/work/python4DS/python4DS/.pixi/envs/default/lib/python3.10/site-packages/IPython/core/pylabtools.py:170: UserWarning: Glyph 44552 (\N{HANGUL SYLLABLE GEUM}) missing from font(s) STIXGeneral.
  fig.canvas.print_figure(bytes_io, **kw)
/home/runner/work/python4DS/python4DS/.pixi/envs/default/lib/python3.10/site-packages/IPython/core/pylabtools.py:170: UserWarning: Glyph 44396 (\N{HANGUL SYLLABLE GU}) missing from font(s) STIXGeneral.
  fig.canvas.print_figure(bytes_io, **kw)
/home/runner/work/python4DS/python4DS/.pixi/envs/default/lib/python3.10/site-packages/IPython/core/pylabtools.py:170: UserWarning: Glyph 49457 (\N{HANGUL SYLLABLE SEONG}) missing from font(s) STIXGeneral.
  fig.canvas.print_figure(bytes_io, **kw)
/home/runner/work/python4DS/python4DS/.pixi/envs/default/lib/python3.10/site-packages/IPython/core/pylabtools.py:170: UserWarning: Glyph 49548 (\N{HANGUL SYLLABLE SO}) missing from font(s) STIXGeneral.
  fig.canvas.print_figure(bytes_io, **kw)
/home/runner/work/python4DS/python4DS/.pixi/envs/default/lib/python3.10/site-packages/IPython/core/pylabtools.py:170: UserWarning: Glyph 53469 (\N{HANGUL SYLLABLE TAEG}) missing from font(s) STIXGeneral.
  fig.canvas.print_figure(bytes_io, **kw)
/home/runner/work/python4DS/python4DS/.pixi/envs/default/lib/python3.10/site-packages/IPython/core/pylabtools.py:170: UserWarning: Glyph 49884 (\N{HANGUL SYLLABLE SI}) missing from font(s) STIXGeneral.
  fig.canvas.print_figure(bytes_io, **kw)
/home/runner/work/python4DS/python4DS/.pixi/envs/default/lib/python3.10/site-packages/IPython/core/pylabtools.py:170: UserWarning: Glyph 50836 (\N{HANGUL SYLLABLE YO}) missing from font(s) STIXGeneral.
  fig.canvas.print_figure(bytes_io, **kw)
/home/runner/work/python4DS/python4DS/.pixi/envs/default/lib/python3.10/site-packages/IPython/core/pylabtools.py:170: UserWarning: Glyph 44552 (\N{HANGUL SYLLABLE GEUM}) missing from font(s) STIXGeneral.
  fig.canvas.print_figure(bytes_io, **kw)
/home/runner/work/python4DS/python4DS/.pixi/envs/default/lib/python3.10/site-packages/IPython/core/pylabtools.py:170: UserWarning: Glyph 44396 (\N{HANGUL SYLLABLE GU}) missing from font(s) STIXGeneral.
  fig.canvas.print_figure(bytes_io, **kw)
/home/runner/work/python4DS/python4DS/.pixi/envs/default/lib/python3.10/site-packages/IPython/core/pylabtools.py:170: UserWarning: Glyph 49457 (\N{HANGUL SYLLABLE SEONG}) missing from font(s) STIXGeneral.
  fig.canvas.print_figure(bytes_io, **kw)
/home/runner/work/python4DS/python4DS/.pixi/envs/default/lib/python3.10/site-packages/IPython/core/pylabtools.py:170: UserWarning: Glyph 49548 (\N{HANGUL SYLLABLE SO}) missing from font(s) STIXGeneral.
  fig.canvas.print_figure(bytes_io, **kw)

이제 다른 빠른 .plot.* 옵션들을 살펴보겠습니다.

막대 그래프(가로 방향은 barh를 사용하고, rot는 레이블의 회전을 설정합니다):

코드 보기
taxis.value_counts("payment").sort_index().plot.bar(title="개수", rot=0);
/home/runner/work/python4DS/python4DS/.pixi/envs/default/lib/python3.10/site-packages/IPython/core/events.py:82: UserWarning: Glyph 44060 (\N{HANGUL SYLLABLE GAE}) missing from font(s) STIXGeneral.
  func(*args, **kwargs)
/home/runner/work/python4DS/python4DS/.pixi/envs/default/lib/python3.10/site-packages/IPython/core/events.py:82: UserWarning: Glyph 49688 (\N{HANGUL SYLLABLE SU}) missing from font(s) STIXGeneral.
  func(*args, **kwargs)
/home/runner/work/python4DS/python4DS/.pixi/envs/default/lib/python3.10/site-packages/IPython/core/pylabtools.py:170: UserWarning: Glyph 44060 (\N{HANGUL SYLLABLE GAE}) missing from font(s) STIXGeneral.
  fig.canvas.print_figure(bytes_io, **kw)
/home/runner/work/python4DS/python4DS/.pixi/envs/default/lib/python3.10/site-packages/IPython/core/pylabtools.py:170: UserWarning: Glyph 49688 (\N{HANGUL SYLLABLE SU}) missing from font(s) STIXGeneral.
  fig.canvas.print_figure(bytes_io, **kw)

다음은 .plot.hist()를 사용하여 히스토그램을 생성합니다.

코드 보기
taxis["tip"].plot.hist(bins=30, title="팁");
/home/runner/work/python4DS/python4DS/.pixi/envs/default/lib/python3.10/site-packages/IPython/core/events.py:82: UserWarning: Glyph 54017 (\N{HANGUL SYLLABLE TIB}) missing from font(s) STIXGeneral.
  func(*args, **kwargs)
/home/runner/work/python4DS/python4DS/.pixi/envs/default/lib/python3.10/site-packages/IPython/core/pylabtools.py:170: UserWarning: Glyph 54017 (\N{HANGUL SYLLABLE TIB}) missing from font(s) STIXGeneral.
  fig.canvas.print_figure(bytes_io, **kw)

박스 플롯:

코드 보기
(taxis[["fare", "tolls", "tip"]].plot.box());

산점도:

코드 보기
taxis.plot.scatter(x="fare", y="tip", alpha=0.7, ylim=(0, None));

EDA를 위한 다른 도구들

pandas와 시각화 패키지들 사이에 여러분이 EDA에 필요한 많은 것들이 있습니다. 하지만 오직 EDA를 더 쉽게 만들기 위해 전용으로 만들어진 도구들도 알아둘 가치가 있습니다.

요약 통계를 위한 skimpy

skimpy 패키지는 대규모 HTML 보고서가 아닌 콘솔에서 데이터 프레임 변수에 대한 요약 통계량을 제공하는 가벼운 도구입니다(이 장의 나머지 부분에 있는 다른 EDA 패키지들이 제공하는 것과는 다릅니다). 때로는 데이터 프레임에 .summary()를 실행하는 것만으로는 충분하지 않을 때가 있는데, skimpy가 그 공백을 메워줍니다. 또한 이전 장에서 본 열 이름을 정리하기 위한 clean_columns() 함수도 함께 제공됩니다. 설치하려면 터미널에서 pixi add skimpy를 실행하세요.

실제로 작동하는 skimpy를 보겠습니다.

코드 보기
skim(taxis)
╭──────────────────────────────────────────────── skimpy summary ─────────────────────────────────────────────────╮
│          Data Summary                Data Types               Categories                                        │
│ ┏━━━━━━━━━━━━━━━━━━━┳━━━━━━━━┓ ┏━━━━━━━━━━━━━┳━━━━━━━┓ ┏━━━━━━━━━━━━━━━━━━━━━━━┓                                │
│ ┃ Dataframe          Values ┃ ┃ Column Type  Count ┃ ┃ Categorical Variables ┃                                │
│ ┡━━━━━━━━━━━━━━━━━━━╇━━━━━━━━┩ ┡━━━━━━━━━━━━━╇━━━━━━━┩ ┡━━━━━━━━━━━━━━━━━━━━━━━┩                                │
│ │ Number of rows    │ 6433   │ │ float64     │ 5     │ │ color                 │                                │
│ │ Number of columns │ 14     │ │ category    │ 4     │ │ payment               │                                │
│ └───────────────────┴────────┘ │ datetime64  │ 2     │ │ pickup_borough        │                                │
│                                │ string      │ 2     │ │ dropoff_borough       │                                │
│                                │ int64       │ 1     │ └───────────────────────┘                                │
│                                └─────────────┴───────┘                                                          │
│                                                     number                                                      │
│ ┏━━━━━━━━━━━━━━━━┳━━━━━━┳━━━━━━━━┳━━━━━━━━━━━┳━━━━━━━━━┳━━━━━━━┳━━━━━━━━┳━━━━━━━━━┳━━━━━━━┳━━━━━━━━┳━━━━━━━━━┓  │
│ ┃ column          NA    NA %    mean       sd       p0     p25     p50      p75    p100    hist    ┃  │
│ ┡━━━━━━━━━━━━━━━━╇━━━━━━╇━━━━━━━━╇━━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━╇━━━━━━━━╇━━━━━━━━━╇━━━━━━━╇━━━━━━━━╇━━━━━━━━━┩  │
│ │ passengers       0     0    1.539  1.204    0     1      1    2     6 █▁  ▁  │  │
│ │ distance         0     0    3.025  3.828    0  0.98   1.64 3.21  36.7  █▁    │  │
│ │ fare             0     0    13.09  11.55    1   6.5    9.5   15   150  █▁    │  │
│ │ tip              0     0    1.979  2.449    0     0    1.7  2.8  33.2 │  │
│ │ tolls            0     0   0.3253  1.415    0     0      0    0 24.02 │  │
│ │ total            0     0    18.52  13.82  1.3  10.8  14.16 20.3 174.8  █▁    │  │
│ └────────────────┴──────┴────────┴───────────┴─────────┴───────┴────────┴─────────┴───────┴────────┴─────────┘  │
│                                                    category                                                     │
│ ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┓  │
│ ┃ column                         NA      NA %                                ordered         unique      ┃  │
│ ┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━┩  │
│ │ color                             0                                 0False                   2 │  │
│ │ payment                          44                0.6839732628633608False                   3 │  │
│ │ pickup_borough                   26                0.4041660189647132False                   5 │  │
│ │ dropoff_borough                  45                 0.699518109746619False                   6 │  │
│ └───────────────────────────────┴────────┴────────────────────────────────────┴────────────────┴─────────────┘  │
│                                                    datetime                                                     │
│ ┏━━━━━━━━━━━━━┳━━━━━━┳━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓  │
│ ┃ column       NA    NA %     first                         last                          frequency     ┃  │
│ ┡━━━━━━━━━━━━━╇━━━━━━╇━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩  │
│ │ pickup        0      0    2019-02-28 23:29:03         2019-03-31 23:43:45     None          │  │
│ │ dropoff       0      0    2019-02-28 23:32:35         2019-04-01 00:13:58     None          │  │
│ └─────────────┴──────┴─────────┴──────────────────────────────┴──────────────────────────────┴───────────────┘  │
│                                                     string                                                      │
│ ┏━━━━━━━━━━━┳━━━━┳━━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━━┳━━━━━━━━━━┓  │
│ ┃                                                                     chars     words per  total    ┃  │
│ ┃ column     NA  NA %       shortest  longest    min       max        per row   row        words    ┃  │
│ ┡━━━━━━━━━━━╇━━━━╇━━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━━╇━━━━━━━━━━┩  │
│ │ pickup_zo260.4041660SoHo    RiverdaleAllertonYorkville    16.2      2.5   15791 │  │
│ │ ne        │    │ 189647132 │          │ /North   /Pelham West      │          │           │          │  │
│ │           │    │           │          │ RiverdaleGardens  │           │          │           │          │  │
│ │           │    │           │          │ /Fieldsto │          │           │          │           │          │  │
│ │           │    │           │          │ n         │          │           │          │           │          │  │
│ │ dropoff_z450.6995181SoHo    RiverdaleAllertonYorkville    16.3      2.5   15851 │  │
│ │ one       │    │  09746619 │          │ /North   /Pelham West      │          │           │          │  │
│ │           │    │           │          │ RiverdaleGardens  │           │          │           │          │  │
│ │           │    │           │          │ /Fieldsto │          │           │          │           │          │  │
│ │           │    │           │          │ n         │          │           │          │           │          │  │
│ └───────────┴────┴───────────┴──────────┴───────────┴──────────┴───────────┴──────────┴───────────┴──────────┘  │
╰────────────────────────────────────────────────────── End ──────────────────────────────────────────────────────╯

요약

이 장에서는 여러분이 데이터 내의 변동을 이해하도록 돕는 다양한 도구들을 배웠습니다. 한 번에 하나의 변수와 작업하는 기법들과 두 개의 변수 쌍과 작업하는 기법들을 보았습니다. 데이터에 수십 개 또는 수백 개의 변수가 있다면 이것이 지나치게 제한적으로 보일 수도 있지만, 이는 다른 모든 기법들이 세워지는 기초입니다.

다음 장에서는 결과를 전달하는 데 사용할 수 있는 도구들에 집중하겠습니다.