[SK플래닛] ASAC 빅데이터전문가 11기 | 21일차

21일차는 깔끔하게 정리된 예제 데이터가 아니라, 실제로는 꽤 자주 만날 법한 지저분한 엑셀/CSV 데이터를 어떻게 코드로 정리할 것인가가 중심이었다. World Happiness Report 데이터를 기준으로 시작했지만, 핵심은 행복지수 자체를 해석하는 것보다 여러 파일을 코드로 불러오고, 서로 다른 연도/출처의 데이터를 병합하고, 안 붙는 데이터를 찾아서 처리하는 과정이었다.

특히 이번에는 데이터가 한 파일에만 있지 않았다. 2022년 행복도 데이터, 2021년 지역 구분 데이터, World Bank 인구 데이터처럼 서로 다른 파일과 출처를 같이 써야 했다. 게다가 국가 이름도 완벽하게 맞지 않았고, 어떤 파일은 연도가 아래로 쌓여 있었는데 다른 파일은 연도가 컬럼으로 옆으로 펼쳐져 있었다. 결국 21일차의 핵심은 데이터 분석 전에 데이터를 분석 가능한 모양으로 만드는 작업이었다.  

1. zip 파일을 코드로 풀고, 파일 경로를 코드로 다루기

가장 먼저 한 일은 압축 파일을 코드로 푸는 것이었다. 기존에는 파일을 하나씩 다운로드해서 쓰는 느낌이었다면, 이번에는 데이터가 data.zip으로 묶여 있었다. Colab이나 Linux 환경에서는 우클릭으로 압축을 푸는 것이 아니라 명령어로 처리해야 하므로, unzip을 사용했다.

!unzip -qq '/content/data.zip' -d '/content/'

여기서 -qq는 압축 해제 과정을 조용히 처리하는 옵션이고, -d는 압축을 어디에 풀지 지정하는 옵션이다. 이번 데이터는 압축을 풀면 /content/data/ 아래에 2021, 2022, population 같은 폴더가 생기는 구조였다. 자료에서도 이번 실습의 목적이 단순히 행복도 내용을 보는 것이 아니라, 압축 파일과 여러 파일을 코드로 다루는 흐름을 보는 것이라고 설명되어 있었다.  

파일 경로를 하나씩 복사해서 쓰는 대신, glob로 규칙에 맞는 파일을 가져오는 방식도 사용했다.

import os
import glob

data_pattern = "/content/data/*/*.*"
glob.glob(data_pattern, recursive=True)

결과는 대략 이런 식이었다.

/content/data/2021/DataForFigure2.1WHR2021C2.xls
/content/data/2022/Appendix_2_Data_for_Figure_2.1.xls
/content/data/2022/DataForTable2.1.xls
/content/data/population/API_SP.POP.TOTL_DS2_en_excel_v2_4770385.xls

특정 폴더와 확장자만 보고 싶으면 패턴을 더 좁혔다.

data_pattern = "/content/data/2022/*.xls"
data_file_list = glob.glob(data_pattern, recursive=True)

data_file_list

이 방식이 좋았던 이유는 파일이 많아졌을 때 매번 경로를 직접 복사하지 않아도 된다는 점이다. 데이터 처리도 결국 반복 작업이 많기 때문에, 파일 접근부터 코드화해두는 게 중요하게 느껴졌다.

2. 2022년 행복도 데이터 불러오기: 엑셀도 바로 믿으면 안 됐다

2022년도 행복도 데이터는 data_file_list[0]에 있는 엑셀 파일을 불러왔다.

import pandas as pd

happy_path = data_file_list[0]
data_happy = pd.read_excel(happy_path)

data_happy.head()
data_happy.tail()
data_happy.info()

처음 head()만 보면 정상적인 데이터처럼 보였다. RANK, Country, Happiness score, Explained by: GDP per capita 같은 컬럼이 있고, 국가별 행복도 점수가 들어 있었다. 그런데 tail()을 보니 마지막 쪽에 이상한 행이 들어가 있었다. 자료에서도 2022년 행복도 데이터는 그해 국가별 행복도 순위 데이터이지만, 맨 마지막 줄에 이상한 데이터가 잘못 들어와 있는 것을 확인해야 한다고 정리되어 있었다.  

data_happy.tail()

info()를 보면 Happiness score 같은 수치 컬럼은 146개만 non-null인데, 전체 행은 147개였다. 이 차이를 통해 마지막에 실제 데이터가 아닌 행이 섞여 있다는 걸 의심할 수 있었다.

RangeIndex: 147 entries, 0 to 146
Happiness score 146 non-null

이 부분에서 다시 느낀 건, 사람이 엑셀로 만진 데이터는 앞부분만 보고 믿으면 안 된다는 점이다. head()뿐 아니라 tail(), info()까지 같이 봐야 한다.

3. 2021년 데이터를 가져온 이유: 지역 정보가 필요했다

2022년 데이터에는 국가명은 있지만, 각 국가가 어느 지역에 속하는지에 대한 정보가 없었다. 예를 들어 Western Europe, South Asia, Sub-Saharan Africa 같은 지역 구분이 빠져 있었다. 그런데 2021년 데이터에는 Regional indicator 컬럼이 있었다.

path_2021 = "/content/data/2021/DataForFigure2.1WHR2021C2.xls"
data_happy_2021 = pd.read_excel(path_2021)

data_happy_2021.head()
data_happy_2021.tail()

이번에 하고 싶었던 것은 2022년 데이터를 기준으로 유지하면서, 2021년 데이터에서 지역 정보만 가져와 붙이는 것이었다.

cols = ["Country name", "Regional indicator"]
data_happy_2021 = data_happy_2021.loc[:, cols]

data_happy_2021.head()

여기서 중요한 건 join을 하기 전에 필요한 컬럼만 남겼다는 점이다. 2021년 데이터의 행복도 점수나 다른 수치 정보는 이번 목적에 필요 없었다. 필요한 건 국가명과 지역 구분뿐이었다.

2022년 데이터: Country, Happiness score 등
2021년 데이터: Country name, Regional indicator
목적: 2022년 데이터에 Regional indicator 추가

자료에서도 join은 데이터가 커질수록 무거운 작업이므로, 병합에 필요한 컬럼만 남기고 compact하게 처리하는 것이 좋다고 설명했다.  

4. left merge를 했지만, 국가명이 완벽히 붙지는 않았다

2022년 데이터를 기준으로 유지해야 했기 때문에 how="left"를 사용했다.

data_happy_tot = pd.merge(
    data_happy,
    data_happy_2021,
    how="left",
    left_on="Country",
    right_on="Country name"
)

data_happy_tot.head()

여기서 left_on="Country"는 2022년 데이터의 국가명 컬럼이고, right_on="Country name"은 2021년 데이터의 국가명 컬럼이다. SQL에서 join을 할 때 key가 필요했던 것처럼, pandas의 merge에서도 연결 기준이 필요했다. 문제는 이 기준이 정확한 코드값이 아니라 사람이 작성한 국가명 문자열이었다는 점이다.

그래서 병합 후에 안 붙은 데이터가 있는지 확인했다.

data_happy_tot.loc[
    data_happy_tot.loc[:, "Country name"].isnull(),
    :
]

처음 확인했을 때 지역 정보가 붙지 않은 데이터가 24개였다.

len(
    data_happy_tot.loc[
        data_happy_tot.loc[:, "Country name"].isnull(),
        :
    ]
)

여기서 나온 값이 24였다. 안 붙은 데이터를 보니 Rwanda*, Yemen*, Botswana*처럼 국가명 뒤에 별표가 붙어 있는 경우가 많았다. 즉, 실제로 같은 나라가 있어도 문자열이 완벽히 같지 않으면 merge가 실패한다.

5. 별표 제거: 정규식 + apply로 국가명 정리하기

국가명 뒤의 * 때문에 병합이 안 되는 경우가 많아 보였기 때문에, 먼저 별표를 제거해보기로 했다. 하나의 샘플로 확인했다.

import re

re.sub(r"\*", "", "Rwanda*")

결과는 "Rwanda"가 된다.

여기서 *는 정규식에서 특수한 의미가 있는 문자라서, 실제 별표 문자로 인식시키려면 \*처럼 escape 처리가 필요했다. 이 규칙이 잘 되는 것을 확인한 뒤, 2022년 데이터의 Country 컬럼 전체에 적용했다.

data_happy.loc[:, "Country"] = data_happy.loc[:, "Country"].apply(
    lambda x: re.sub(r"\*", "", x)
)

data_happy.tail()

이후 다시 merge를 수행했다.

data_happy_tot = pd.merge(
    data_happy,
    data_happy_2021,
    how="left",
    left_on="Country",
    right_on="Country name"
)

data_happy_tot.loc[
    data_happy_tot.loc[:, "Country name"].isnull(),
    :
]

그 결과 안 붙는 데이터가 24개에서 4개로 줄었다. 남은 것은 Czechia, Congo, Eswatini, Kingdom of, 그리고 마지막의 이상한 xx 행이었다. 여기서 완전히 자동으로 해결할 수 없는 경우가 남는다는 점도 보였다.

처음부터 모든 국가명을 손으로 고치기보다, 먼저 규칙을 찾아서 최대한 줄이고, 정말 안 되는 것만 수작업으로 확인하는 방향이 맞아 보였다.

전체 24개를 손으로 처리하기보다
→ 별표 제거 규칙을 적용
→ 남은 4개만 확인

6. 결측 행 제거와 인덱스 문제

진행 편의를 위해 지역 정보가 붙지 않은 행은 제거했다.

data_happy_tot.dropna(subset=["Country name"], inplace=True)

data_happy_tot.shape

결과는 143행이었다.

147개
→ 4개 제거
→ 143개

그런데 여기서 중요한 주의점이 있었다. 행을 제거하면 기존 index가 그대로 남는다. 즉 데이터 개수는 143개인데, index는 0부터 145까지 남아 있을 수 있다.

data_happy_tot.info()
data_happy_tot.tail()

자료에서도 이 부분을 강조했다. 데이터를 지우면 정수 index가 중간중간 빠질 수 있고, 이후 iloc를 사용할 때 문제가 생길 수 있다. 실제로 상위 10개와 하위 10개를 뽑는 과정에서 이 문제가 터졌다.  

처음에는 아래처럼 접근했다.

cols = ["Country", "Happiness score"]
cnt = 10

top_low = list(data_happy_tot.head(cnt).index) + list(data_happy_tot.tail(cnt).index)

data_happy_tot.loc[:, cols].sort_values(
    by="Happiness score",
    ascending=False
).iloc[top_low, :]

그런데 IndexError: positional indexers are out-of-bounds가 발생했다. 이유는 top_low에 들어 있는 값은 label index인데, iloc는 위치 기반으로 접근하기 때문이다. 행을 삭제하면서 index가 중간중간 비어 있었고, 이 상태에서 iloc로 접근하니 위치 범위를 벗어나는 문제가 생겼다.

자료에서도 행을 삭제하면 내부적인 정수 인덱스가 꼬일 수 있고, 이런 경우에는 reset_index()를 고려해야 한다고 설명했다.  

이 경우에는 label 기반 접근인 loc를 사용하면 원하는 결과를 볼 수 있었다.

data_happy_tot.loc[:, cols].sort_values(
    by="Happiness score",
    ascending=False
).loc[top_low, :]

이 부분이 꽤 크게 남았다. 겉으로 숫자처럼 보이는 index가 항상 iloc의 위치 번호와 같은 것은 아니다. 데이터를 삭제한 뒤에는 reset_index()를 하거나, 내가 쓰는 접근 방식이 loc인지 iloc인지 정확히 구분해야 한다.

7. 지역별 분포와 정적 시각화

데이터를 어느 정도 정리한 뒤에는 지역별로 몇 개의 국가가 조사되었는지 확인했다. 처음에는 seaborn.countplot()을 사용했다.

import matplotlib.pyplot as plt
import seaborn as sns

sns.countplot(
    data=data_happy_tot,
    x="Regional indicator"
)

plt.xticks(rotation=90)
plt.show()

지역명이 길어서 x축 라벨이 겹쳤기 때문에 rotation=90으로 돌렸다. 이후에는 값이 많은 순서대로 정렬해서 보기 위해 value_counts().index를 order에 넣었다.

sns.countplot(
    data=data_happy_tot,
    x="Regional indicator",
    order=data_happy_tot.loc[:, "Regional indicator"].value_counts().index
)

plt.xticks(rotation=90)
plt.show()

이 방식보다 더 간단하게는 pandas의 plot(kind="bar")를 사용할 수도 있었다.

data_happy_tot.loc[:, "Regional indicator"].value_counts().plot(kind="bar")

이 부분은 같은 결과를 그리더라도 seaborn을 쓸지, pandas plot을 쓸지 선택할 수 있다는 점이 보였다. 빠르게 확인할 때는 pandas plot도 괜찮고, 세부 조정이 필요하면 seaborn이나 matplotlib을 더 쓰면 될 것 같다.

8. boxplot과 KDE로 행복도 분포 보기

그다음에는 행복도 점수와 GDP 관련 컬럼의 분포를 확인했다.

cols = ["Happiness score", "Explained by: GDP per capita"]

sns.boxplot(
    data=data_happy_tot.loc[:, cols]
)

x, y를 따로 지정하지 않아도, 수치형 컬럼만 모아둔 DataFrame을 넘기면 각 컬럼에 대해 boxplot을 그릴 수 있었다. 이후에는 subplots()를 사용해서 각 컬럼을 따로 그리고, y축 범위를 통일했다.

cols = ["Happiness score", "Explained by: GDP per capita"]

fig, ax = plt.subplots(nrows=1, ncols=len(cols))

temp = []

for idx, col in enumerate(cols):
    sns.boxplot(
        data=data_happy_tot,
        y=col,
        ax=ax[idx]
    )
    temp.append(min(data_happy_tot.loc[:, col]))
    temp.append(max(data_happy_tot.loc[:, col]))

gap = 5

for i_ax in ax:
    i_ax.set_ylim(min(temp) - gap, max(temp) + gap)

plt.show()

지역별 행복도 분포는 kdeplot()으로 봤다.

sns.kdeplot(
    data=data_happy_tot,
    x="Happiness score"
)

전체 국가를 한 번에 보면 대략적인 분포가 보이고, 지역별 차이를 보고 싶을 때는 hue를 사용했다.

plt.figure(figsize=(15, 10))

sns.kdeplot(
    data=data_happy_tot,
    x="Happiness score",
    hue="Regional indicator"
)

평균 행복도를 기준선으로 넣을 수도 있었다.

plt.figure(figsize=(15, 10))

sns.kdeplot(
    data=data_happy_tot,
    x="Happiness score",
    hue="Regional indicator",
    fill=True,
    lw=3
)

plt.axvline(
    data_happy_tot.loc[:, "Happiness score"].mean(),
    color="black",
    lw=2
)

이렇게 보니까 단순히 “평균이 얼마다”보다 지역별 분포가 어떻게 겹치고 벌어지는지 보는 게 훨씬 직관적이었다.

9. Plotly로 동적인 지도 시각화하기

이번에는 정적인 그래프뿐 아니라 Plotly도 다뤘다. Plotly는 matplotlib/seaborn처럼 이미지를 고정해서 보여주는 방식이 아니라, 마우스를 올리면 값이 뜨고 확대/축소가 되는 동적인 그래프를 만들 수 있다. 자료에서도 Plotly는 웹페이지와 연동되는 동적 시각화가 가능하고, Tableau처럼 마우스를 올렸을 때 값을 확인할 수 있는 그래프를 코드로 만들 수 있다고 설명했다.  

import plotly.express as px
import plotly.graph_objects as go
from plotly.offline import init_notebook_mode, iplot

Plotly에는 크게 express와 graph_objects 방식이 있었다. plotly.express는 데이터를 넣으면 비교적 쉽게 그래프를 그릴 수 있지만 세밀한 조정은 제한적이고, graph_objects는 더 세부적인 커스터마이징이 가능하지만 코드가 복잡해질 수 있다. 자료에서도 express는 편하지만 미세 조정이 어렵고, graph 방식은 세부 조정은 가능하지만 코드가 불편할 수 있다고 설명했다.  

이번에는 국가별 행복지수를 지도에 표현했다. locations에는 국가명을 넣고, color에는 행복지수인 Life Ladder를 넣었다. 시간 흐름을 보려고 animation_frame에는 연도를 넣었다.

fig = px.choropleth(
    data_happy_log,
    locations="Country name",
    color="Life Ladder",
    locationmode="country names",
    animation_frame="year"
)

fig.show()

처음에는 연도 순서가 이상하게 재생될 수 있었다. 데이터가 연도순으로 정렬되어 있지 않으면 Plotly가 들어온 순서대로 프레임을 만들기 때문이다. 그래서 연도 기준으로 정렬한 뒤 다시 그렸다.

data_happy_log_sort = data_happy_log.sort_values(by="year")

fig = px.choropleth(
    data_happy_log_sort,
    locations="Country name",
    color="Life Ladder",
    locationmode="country names",
    animation_frame="year"
)

fig.show()

이 부분이 좋았던 이유는 국가, 연도, 행복지수라는 세 가지 정보를 하나의 지도에서 같이 볼 수 있었기 때문이다. 지도 위에서 나라별 색이 시간에 따라 바뀌고, 마우스를 올리면 해당 국가와 수치를 확인할 수 있었다. 자료에서도 locations, color, locationmode, animation_frame을 이용해 국가명, 행복지수, 연도 정보를 하나의 동적 지도에 넣는 구조를 설명했다.  

10. World Bank 인구 데이터: 방향이 다른 데이터가 나왔다

후반부에서는 World Bank에서 가져온 인구 데이터를 붙이는 작업을 했다. 이 데이터는 기존 행복도 데이터와 출처가 달랐다. 그래서 국가명을 기준으로 매칭해야 했는데, 이때 또 국가명 불일치 문제가 생길 수 있었다.

더 큰 문제는 데이터 모양이었다. 행복도 로그 데이터는 연도가 아래로 쌓여 있었다.

Afghanistan 2008
Afghanistan 2009
Afghanistan 2010
...

반면 World Bank 인구 데이터는 연도가 컬럼으로 옆으로 펼쳐져 있었다.

Country Name | 1960 | 1961 | 1962 | ... | 2021

자료에서도 공공 데이터에서는 이런 식으로 연도가 컬럼으로 옆으로 펼쳐진 경우가 많고, 내가 원하는 분석 모양과 데이터가 쌓인 방향이 다를 수 있다고 설명했다.  

엑셀 파일을 그냥 읽으면 앞쪽 설명 행 때문에 컬럼이 밀려 들어왔다. 그래서 skiprows를 사용해 실제 데이터가 시작하는 위치부터 읽었다.

pop_path = "/content/data/population/API_SP.POP.TOTL_DS2_en_excel_v2_4770385.xls"

pop_df = pd.read_excel(
    pop_path,
    skiprows=3
)

pop_df.head()

이번 목표는 data_happy_log의 각 행에 해당 국가와 해당 연도의 인구 정보를 새 컬럼으로 추가하는 것이었다.

행복도 로그 데이터의 한 행:
Afghanistan, 2008

World Bank 데이터에서 찾을 값:
Country Name == Afghanistan
column == "2008"

11. 반복문으로 인구 정보 붙이기

먼저 data_happy_log에 인구 컬럼을 만들었다.

data_happy_log["pop"] = 0

그다음 각 행을 돌면서 국가명과 연도를 가져오고, World Bank 데이터에서 해당 국가/연도 값을 찾아 넣는 구조였다.

add_col = data_happy_log.columns.get_loc("pop")

반복문 안에서는 한 행씩 가져와서 나라와 연도를 뽑았다.

for i in range(len(data_happy_log)):
    i_data = data_happy_log.iloc[i, :]

    country = i_data["Country name"]
    year = str(i_data["year"])

    temp = pop_df.loc[
        pop_df.loc[:, "Country Name"] == country,
        year
    ]

    if len(temp) != 0:
        data_happy_log.iat[i, add_col] = temp.values[0]

여기서 year를 문자열로 바꾼 이유는 World Bank 데이터에서 연도 컬럼명이 숫자가 아니라 문자열 형태로 들어가 있었기 때문이다. 또 하나의 셀에 값을 넣는 것이므로 iat를 사용했다. 자료에서도 하나의 위치에 빠르게 값을 기록할 때는 loc보다 iat/at 계열을 쓰는 것이 더 적합하다고 설명했다.  

이 코드는 이전에 영화 상세 정보를 API로 가져와서 옆에 컬럼을 추가하던 구조와 비슷했다. 메인 데이터 한 행을 보고, 외부 데이터에서 필요한 값을 찾아와서, 메인 데이터에 새 속성으로 붙이는 방식이었다.

12. 안 붙은 인구 데이터 확인과 국가명 보정

인구 정보를 붙인 뒤에는 값이 0으로 남아 있는 행을 확인했다.

data_happy_log.loc[
    data_happy_log.loc[:, "pop"] == 0,
    :
]

인구 정보가 안 붙은 행이 꽤 있었다. 전체 2089개 중 일부가 매칭되지 않았고, 이는 대부분 행복도 데이터와 World Bank 데이터에서 국가명을 다르게 쓰기 때문이었다. 예를 들어 한쪽은 Yemen이라고 쓰고, 다른 쪽은 Yemen, Rep.처럼 쓰는 식이다.

그래서 문제가 되는 국가명이 몇 종류인지 먼저 확인했다.

missing_country = data_happy_log.loc[
    data_happy_log.loc[:, "pop"] == 0,
    "Country name"
].unique()

len(missing_country)

그리고 어떤 국가가 많이 문제가 되는지 보기 위해 value_counts()도 사용했다.

data_happy_log.loc[
    data_happy_log.loc[:, "pop"] == 0,
    "Country name"
].value_counts()

이 부분에서 중요한 건 236개 행을 다 보는 것이 아니라, unique한 국가명 단위로 줄여서 봐야 한다는 점이었다. 행 기준으로 보면 많아 보여도, 실제로는 몇 개 국가명만 잘 보정하면 여러 행이 한 번에 해결될 수 있다.

국가명 보정은 딕셔너리와 함수를 만들어 처리했다.

country_rename = {
    "Yemen": "Yemen, Rep.",
    "Egypt": "Egypt, Arab Rep.",
    "Russia": "Russian Federation",
    "South Korea": "Korea, Rep."
}
def change_country_name(country):
    if country in country_rename.keys():
        return country_rename[country]
    else:
        return country

기존 국가명은 보존하고, World Bank와 매칭하기 위한 국가명 컬럼을 새로 만들었다.

data_happy_log["pop_Country name"] = data_happy_log["Country name"]

data_happy_log["pop_Country name"] = data_happy_log["pop_Country name"].apply(
    lambda x: change_country_name(x)
)

이 부분은 전처리에서 꽤 현실적인 지점이었다. 서로 다른 출처의 데이터를 붙일 때는 완벽한 foreign key가 없는 경우가 많고, 결국 국가명 같은 문자열을 기준으로 붙여야 할 때가 있다. 이때는 안 붙는 데이터를 확인하고, 규칙을 만들고, 그래도 남는 것은 직접 판단해야 했다. 자료에서도 서로 다른 기관의 데이터는 연결 기준이 완벽하지 않을 수 있고, 분석자가 직접 확인하면서 룰을 잡아야 한다고 설명했다.  

마무리

21일차는 데이터 분석 자체보다, 분석을 하기 전에 데이터를 쓸 수 있는 상태로 만드는 작업이 중심이었다. 압축 파일을 풀고, glob로 여러 파일 경로를 가져오고, 엑셀 파일을 읽은 뒤 head, tail, info, describe로 데이터 상태를 확인했다. 2022년 행복도 데이터에는 지역 정보가 없어서 2021년 데이터를 가져와 붙였고, 국가명 뒤의 별표 때문에 merge가 실패하는 문제를 정규식과 apply로 줄였다.

그 과정에서 행을 삭제하면 index가 꼬일 수 있고, 이 상태에서 iloc를 쓰면 문제가 생길 수 있다는 것도 직접 확인했다. 이후에는 지역별 countplot, boxplot, KDE plot으로 분포를 봤고, Plotly를 이용해 연도별 행복지수를 동적인 지도에 표현했다.

마지막으로 World Bank 인구 데이터를 붙이면서, 서로 다른 출처의 데이터는 모양도 다르고 국가명도 다르게 들어올 수 있다는 점을 봤다. 행복도 데이터는 연도가 행으로 쌓여 있었지만, 인구 데이터는 연도가 컬럼으로 펼쳐져 있었다. 그래서 한 행씩 돌면서 국가명과 연도에 맞는 값을 찾아 붙였고, 매칭되지 않는 국가명은 unique/value_counts로 줄인 뒤 딕셔너리 기반으로 보정했다.


21일차는 깔끔한 데이터로 분석하는 날이 아니라, 실제 데이터처럼 흔들리는 파일 구조와 국가명 불일치를 코드로 줄여가며 분석 가능한 형태로 만드는 흐름을 본 날이었다.