Visualization of Uncertainty

  • 데이터의 불확실성을 함께 보여주는 방법 중 하나로 오차 막대나 신뢰 구간을 사용합니다.
  • 오차 막대는 데이터 하나 하나에 붙여서, 신뢰 구간은 전체적인 범위를 보여줍니다.
  • matplotlib과 seaborn으로 불확실성을 도시하는 방법을 정리했습니다.

0. 데이터 생성

  • 평균이 0이고 표준편차가 1인 데이터를 100개씩 21쌍을 만듭니다.
  • numpy.random.normal()를 이용해서 데이터 프레임으로 한번에 만들 수 있습니다.
    1
    2
    df_test = pd.DataFrame({"X":np.concatenate([[i]*100 for i in range(21)]), "Y":np.random.normal(size=2100)})
    df_test.head()
  • 데이터 100개씩의 평균과 표준편차를 구합니다.
  • 로우 데이터와 별도의 데이터프레임이 하나 생겼습니다.
    1
    2
    3
    4
    df_gX_mean = df_test.groupby("X").mean().reset_index()
    df_gX_std = df_test.groupby("X").std().reset_index()
    df_gX = df_gX_mean.merge(df_gX_std, left_on="X", right_on="X", suffixes=("_mean", "_std"))
    df_gX.head()

1. 오차 막대 error bar 그리기

matplotlib.pyplot.errorbar
matplotlib.axes.Axes.errorbar
seaborn.lineplot

  • 오차 막대는 matplotlib의 errorbar명령으로 표현할 수 있습니다.
    • scatter plot과 line plot을 모두 그릴 수 있는 plot()함수의 확장형으로 생각해도 무방합니다.
    • 오차 막대를 표현할 수 있는 옵션을 담고 있기 때문에 인자가 훨씬 많습니다.
    • 상태기반 방식객체지향 방식모두 동일한 명령으로 사용 가능합니다.
    • 그러나 객체지향 방식 페이지에서 제공하는 예제가 훨씬 다양하고 많습니다.
  • 같은 데이터에 다른 옵션으로 그려봅니다.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    fig, axs = plt.subplots(ncols=3, figsize=(10,3), sharex=True, sharey=True)

    axs[0].errorbar(x=df_gX["X"], y=df_gX["Y_mean"], yerr=df_gX["Y_std"], label="case I")
    axs[0].set_title("default settings")
    axs[0].legend()

    axs[1].errorbar(x=df_gX["X"], y=df_gX["Y_mean"], yerr=df_gX["Y_std"],
    fmt="ro", elinewidth=0.5, capsize=2, label="case II")
    axs[1].set_title("fmt, elinewith, capsize")
    axs[1].legend(loc="upper right")

    axs[2].errorbar(x=df_gX["X"], y=df_gX["Y_mean"], yerr=df_gX["Y_std"],
    ls="none", marker="s", mec="k", mfc="w", elinewidth=0.5, capsize=2,
    ecolor="magenta", errorevery=3, label="case III")
    axs[2].set_title("ls, marker, ecolor, errorevery")
    axs[2].legend(loc="upper right")
  • Case I : 기본 설정
    • line plot에 error bar가 line 형태로 붙어 있습니다.
    • 간결해서 보기 좋기도 하지만 제 분야에서 일반적으로 쓰는 모양은 아니라 어색합니다.
  • Case II : fmt, elinewidth, capsize

    • scatter plot에 error bar가 $\text{I}$자 형태로 붙어 있습니다.
    • fmt(format) 인자로 형태를 제어합니다.
    • capsize로 error bar 모양을 $\text{I}$자로 만들어 익숙하게 합니다.
    • 평소에 자주 보는 형식이라 익숙합니다.
  • Case III : line(ls, c), marker(marker, mec, mfc)

    • scatter + line plot에 error bar가 $\text{I}$ 형태로 붙어 있습니다.
    • line과 marker를 별도로 제어합니다.
    • 오차 막대가 너무 빼곡해서 보기 어려우면 errorevery로 제어할 수 있습니다.
    • 목적에 맞는 그래프로 만들 수 있습니다.
  • seaborn에서도 seaborn.lineplot()명령어를 이용해 오차 막대를 표현할 수 있습니다.

    • 그러나 사실상 matplotlib의 errorbar를 쓰는 것과 별반 다르지 않습니다.
    • err_kws인자에 dictionary 형식으로 error bar 관련 인자들을 집어넣어 줘야 하는데, 여기 사용되는 인자들이 seaborn 내부에서 matplotlib의 errorbar()에 전달될 인자이기 때문입니다.
    • 바꿔 말하면 seaborn으로 그리려면 matplotlib으로 그릴 줄 알아야 한다는 뜻입니다.

2. 오차 밴드

  • 데이터마다 오차 막대를 붙이는 대신 전체적으로 오차 범위를 밴드로 표시하기도 합니다.
  • 다수의 선을 하나의 면으로 표현하기 때문에 시각적으로 덜 번잡하여 최근 많이 선호되는 방법입니다.

wikipedia: confidence interval
minitab: 신뢰 구간의 정의

  • 오차 막대는 표준 편차를 보여준다면, 오차 밴드신뢰 구간(ci: confidence interval)을 주로 표현합니다.

  • 신뢰 구간은 표본으로 눈에 보이지 않는 모수를 추정할 때 얼마나 믿을 수 있을지를 표현합니다.

  • 예를 들어 신뢰 구간이 95%라면 “표본을 20번 뽑았을 때 이 중 19번은 모집단의 모수를 포함할 것이다”라는 의미입니다.
    신뢰구간의 정의. 이미지=minitab

  • 신뢰 구간은 다음과 같이 Z-Score $Z$표본의 표준편차 $s$, 그리고 표본의 수 $n$에 따라 결정됩니다.

matplotlib.lines.Line2D

  • 앞에서 만든 데이터를 오차 밴드로 표현해봅시다.
    • maplotlib은 fill_between()을 사용합니다.
    • seaborn은 lineplot()을 사용합니다.
    • 두 라이브러리의 기본값이 다른 점에 유의해서 봅니다.
      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
      cis = [95, "sd"]
      font_title = {"fontsize":12}
      titles = ["ci=95 (seaborn default)", "ci=\"sd\" (standard deviation)"]

      fig, axs = plt.subplots(ncols=3, figsize=(10,3))

      for ax, ci, title in zip(axs[1:], cis, titles[:2]):
      sns.lineplot(x="X", y="Y", data=df_test, ci=ci, ax=ax)
      ax.set_title(title, fontdict=font_title)
      ax.axhline(0, c="green", alpha=0.5)
      ax.set_xlim(0,20)
      ax.set_ylim(-1.25, 1.25)

      for i in range(21):
      if i == 5:
      color = "k"
      alpha = 1

      else:
      color = f"C{i}"
      alpha = 0.2

      df_test.loc[100*i:100*(i+1), "Y"].plot.kde(ax=axs[0], c=color, legend=False, alpha=alpha, zorder=2)
      axs[0].set_xlim(-3,3)
      axs[0].set_ylim(0, 0.5)

      if i == 5:
      axs[0].axvline(0, c="green", alpha=0.5, zorder=3)
      std = df_test.loc[100*i:100*(i+1), "Y"].std()
      ci95 = 1.96 * std/np.sqrt(100)
      mean = df_test.loc[100*i:100*(i+1), "Y"].mean()
      line_mean = axs[0].plot([mean, mean], [0, 0.36], ":", c="k", label="no.7 mean", zorder=2.5)

      # Line plot에서 데이터 가져오기
      X_, Y_ = axs[0].get_lines()[i].get_data(orig=True)
      axs[0].fill_between(X_[(X_ >= mean-ci95) & (X_ < mean+ci95)], Y_[(X_ >= mean-ci95) & (X_ < mean+ci95)], fc="gray", alpha=0.5, zorder=2.4)

      fig.text(0.31, 0.65, f"No. {i}\nmean : {mean:0.2f}\nst.dev : {std:0.2f}\nci (95%) : {ci95:0.2f}", fontdict={"fontsize":10, "color":"k"}, ha="right") #, transform=ax.transAxes)

      fig.tight_layout()
  • 세 칸의 그림 중 맨 왼쪽에는 21쌍 데이터의 분포seaborn.kdeplot으로 그렸습니다.
    • 0을 중심으로 21개의 분포가 퍼져 있는 가운데 5번째 데이터를 굵게 그렸습니다.
    • 평균이 -0.22로 크게 벗어나 있고, ci(95%)가 0.19입니다.
      평균 참값 0이 ci (-0.41 ~ -0.03)안에 들어와 있지 않습니다.
    • kdeplot의 데이터를 가져오는데 .get_lines().get_data()를 사용했습니다.
      관련 내용은 추후 기회가 되면 자세히 다루겠습니다.
  • 가운데 그림에 21쌍 데이터의 평균과 ci를 연속적으로 표현했습니다.
    • 21개 경우 중 한 가지 경우에서 모수의 평균을 제대로 추정하지 못했습니다.
    • 확률로 95%에 가깝습니다. 그러나 21가지라는 제한적인 시도의 결과입니다.
      여러 번 반복 수행하면 결과가 달라집니다.
    • seaborn.lineplot()으로 그렸습니다. 기본적으로 ci를 보여줍니다.
  • 맨 오른쪽에는 같은 데이터의 오차 범위를 표준 편차로 그렸습니다.
    • 훨씬 넓어집니다.
    • 정확히는 가운데 그림이 표준편차에 Z-Score와 표본의 수가 반영된 만큼 폭이 바뀐 것입니다.
    • matplotlib.fill_between()으로 그렸습니다.
    • seaborn.lineplot()ci="sd"로 넣으면 동일한 그림이 나타납니다.

3. 실제 데이터 적용

Ocean salinity: Climate change is also changing the water cycle
Cheng L., K. E. Trenberth, N. Gruber, J. P. Abraham, J. Fasullo, G. Li, M. E. Mann, X. Zhao, Jiang Zhu, 2020: Improved estimates of changes in upper ocean salinity and the hydrological cycle. Journal of Climate. In press, doi: https://doi.org/10.1175/JCLI-D-20-0366.1.

  • 1960년 이후 해수의 염도 변화를 나타낸 그래프입니다.
  • 데이터의 평균값표준편차로 나타낸 오차값이 표현되어 있습니다.

    Figure 3. Increasing salinity-​contrast in the world’s ocean. Figure shows Salinity-​contrast time series from 1960 to 2017 at upper 2000m. Background Photograph: Xilin Wang.
  • 이 그림을 배경 사진을 제외하고 재현해 보겠습니다.
  • 저자들이 올린 데이터를 형식만 수정하여 사용합니다.
  • 여기에서 다운로드받을 수 있습니다.
    1
    2
    data = pd.read_csv("data_2000.txt")
    data.head()
  • 컬럼 이름이 너무 길어서 어렵습니다. 짧게 줄여줍시다.
    1
    2
    3
    4
    cols_0 = data.columns
    cols_1 = ["year", "mean", "2std", "mean_240"]
    data = data.rename(columns=dict(zip(cols_0, cols_1)))
    data.head()

3.1. Matplotlib으로 그리기

  • 오차 범위로 사용할 데이터가 필요합니다.
  • 평균에서 표준편차를 더하고 뺀 값을 추가해줍니다.
  • 이 구간 사이를 오차범위로 도시할 것입니다.
    1
    2
    3
    data["+2s"] = data["mean"] + data["2std"]
    data["-2s"] = data["mean"] - data["2std"]
    data.head()
  • 결과물을 먼저 보여드리겠습니다.


  • 이렇게 그렸습니다.
    1. 배경색 : fig.set_facecolor(), ax.set_facecolor()를 사용합니다.
    2. 공간 분할 : 그래프 아래에 글자를 넣다가 layout이 망가지기 쉽습니다.
      애초에 subplot을 위와 아래에 두 개 만들고 아래는 그래프 대신 글자를 넣으면 안정적입니다.
      비대칭 공간 분할은 gridspec_kw를 이용합니다.
    3. 오차 밴드 : “+2s”와 “-2s” 사이를 fill_between()으로 채웁니다.
    4. Legend : ax.legend() 대신 ax.text()를 사용했습니다.
      하늘색 사각형은 patches.Rectangle()ax.add_patch()했습니다.
    5. labels 제외 글자들 : ax.text()를 사용했습니다.
      텍스트 위치를 axes 내부 상대좌표로 잡으면 편합니다. transform=ax.transAxes를 사용하면 됩니다.
    6. 아래 글자 : fig.text()를 사용했습니다.
      그래프에 관계 없이 전체 그림을 보고 위치를 잡는 게 좋기 때문입니다.

3.2. Seaborn으로 그리기

  • seaborn.lineplot은 오차 범위가 필요하지 않습니다.
  • 해당 범위의 오차를 만드는 데이터가 필요합니다.
  • 각 지점마다 1000개의 데이터를 주어진 오차에 따라 만들어 주었습니다.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    from copy import deepcopy

    def random(loc, scale, size=100):
    return np.random.normal(loc=loc, scale=scale, size=[size]*loc.size)

    data_ = deepcopy(data)
    data_["generated"] = data_.apply(lambda x: random(x["mean"], x["2std"], 1000), axis=1)
    data1 = data_.explode("generated")
    data1["generated"] = data1["generated"].astype("float")
    data1.head()
  • 그러면, matplotlib과 똑같은 그래프를 그릴 수 있습니다.


3.3. Visualization code

  • 소스 코드는 다음과 같습니다.
  • matplotlib과 seaborn으로 그릴 때 각기 activate 하는 부분만 다릅니다.
    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
    fig, axs = plt.subplots(nrows=2, figsize=(10,5), gridspec_kw={"height_ratios":[9,1]})
    fig.set_facecolor("#005FA1")
    axs[0].set_facecolor("#005FA1")

    ### matplotlib
    # axs[0].fill_between(data["year"], data["+2s"], data["-2s"], fc="lightskyblue")
    # axs[0].plot(data["year"], data["mean"], c="w", lw=2)

    ### seaborn
    g = sns.lineplot(x="year", y="generated", data=data1, ci="sd", ax=axs[0], err_kws={"fc":"lightskyblue", "ec":"none", "alpha":1}, color="w", lw=2)

    axs[0].spines["right"].set_visible(False)
    axs[0].spines["top"].set_visible(False)
    axs[0].spines["left"].set_linewidth(2)
    axs[0].spines["left"].set_color("w")
    axs[0].spines["bottom"].set_linewidth(2)
    axs[0].spines["bottom"].set_color("w")

    axs[0].set_xticks([1960, 1970, 1980, 1990, 2000, 2010, 2018])
    axs[0].xaxis.set_minor_locator(MultipleLocator(5))
    axs[0].tick_params(axis="both", which="major", length=5, color="w", labelsize=14, labelcolor="w")
    axs[0].tick_params(which='minor', length=3, color='w')
    axs[0].set_xlabel("")
    axs[0].set_ylabel("(g kg$^{-1}$)", fontdict={"fontsize":14, "color":"w"})
    axs[0].set_xlim(1960, 2018)
    axs[0].set_ylim(-0.023, )

    Rectangle = patches.Rectangle((0.2, 0.94), 0.05, 0.05, color="lightskyblue", transform=axs[0].transAxes)
    axs[0].text(0.02, 0.95, "Monthly mean", fontdict={"fontsize":12, "color":"w"}, transform=axs[0].transAxes)
    axs[0].add_patch(Rectangle)
    axs[0].text(0.26, 0.95, "(2-$\sigma$ error)", fontdict={"fontsize":12, "color":"w"}, transform=axs[0].transAxes)
    axs[0].text(1, 0.06, "Baseline 2008-2017", ha="right", transform=axs[0].transAxes, fontdict={"fontsize":14, "color":"lightgray"})

    axs[1].axis("off")
    fig.text(0.03, 0.1, "Salinity-Contrast time series (SC2000)", fontdict={"fontsize":16, "fontweight":"bold", "color":"w"})
    fig.text(0.03, 0.05, "SC index = mean salinity (regions with S > global mean) - mean salinity (regions with S < global mean)", fontdict={"fontsize":12, "color":"w"})


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

Share