Skip to content

Pandas Melt: 와이드 데이터를 롱 포맷으로 리쉐이프하기 (완전 가이드)

Updated on

각 월이 각각의 컬럼으로 들어있는 스프레드시트를 가지고 있다고 해봅시다. January, February, March… December까지. 수익 숫자가 12개 컬럼에 흩어져 있고, 이를 시계열로 플로팅하거나, groupby를 하거나, 머신러닝 모델에 넣어야 합니다. 그런데 이런 작업들은 wide-format 데이터에서는 잘 동작하지 않습니다. 월 이름을 담는 컬럼 하나와 수익 값을 담는 컬럼 하나가 필요합니다. 이 변환—즉 컬럼을 행으로 바꾸는 작업—을 정확히 해주는 것이 pandas melt입니다.

문제는 금방 더 복잡해집니다. 설문 데이터, 센서 측정값, 재무 보고서, Excel이나 SQL에서 피벗된 형태로 내보낸 데이터처럼 수십~수백 개 컬럼을 가진 wide 데이터셋은 흔합니다. 이를 수동으로 재구성하는 건 번거롭고 실수도 나기 쉽습니다. unpivot해야 할 컬럼이 하나 늘어날 때마다, 제대로 된 도구 없이 처리하려면 보일러플레이트 코드가 계속 늘어납니다.

pd.melt() 함수는 이를 한 번의 호출로 해결합니다. wide DataFrame을 받아 선택한 컬럼들을 행으로 “녹여(melt)” long 포맷으로 바꾸면서, 식별자(identifier) 컬럼은 그대로 보존합니다. 이 가이드에서는 전체 문법, 모든 파라미터, 실전 시나리오 기반 예제, 그리고 pivot, stack, wide_to_long 같은 관련 리쉐이핑 함수들과의 비교까지 다룹니다.

📚

pd.melt()가 하는 일

pd.melt()는 DataFrame을 wide format에서 long format으로 unpivot합니다. wide format에서는 각 변수가 자기 컬럼을 하나씩 가집니다. long format(“tidy” 포맷이라고도 함)에서는 변수 이름을 담는 컬럼 하나와 값(value)을 담는 컬럼 하나가 있습니다.

개념적으로는 다음과 같은 변환입니다:

Wide format (melt 전):

studentmathscienceenglish
Alice908588
Bob789280

Long format (melt 후):

studentsubjectscore
Alicemath90
Alicescience85
Aliceenglish88
Bobmath78
Bobscience92
Bobenglish80

여기서 student 컬럼은 식별자로 유지되고, 3개의 과목 컬럼은 두 개 컬럼으로 melt됩니다: 하나는 과목명(subject), 다른 하나는 점수(score)를 담습니다.

pd.melt() 문법과 파라미터

pd.melt(frame, id_vars=None, value_vars=None, var_name=None,
        value_name='value', col_level=None, ignore_index=True)

DataFrame 메서드로도 호출할 수 있습니다:

df.melt(id_vars=None, value_vars=None, var_name=None,
        value_name='value', col_level=None, ignore_index=True)

파라미터 레퍼런스

ParameterDescriptionDefault
framemelt할 DataFrame (df.melt()를 쓰면 필요 없음)Required
id_vars식별자 변수로 유지할 컬럼(들) (melt되지 않음)None
value_vars행으로 unpivot할 컬럼(들). 생략하면 id_vars에 없는 모든 컬럼이 사용됨None
var_name기존 컬럼명(변수명)을 담는 새 컬럼의 이름'variable'
value_name값(value)을 담는 새 컬럼의 이름'value'
col_level컬럼이 MultiIndex일 때, melt할 레벨None
ignore_indexTrue면 결과가 새 정수 인덱스를 사용. False면 기존 인덱스를 유지True

기본 예제: 학생 성적

위의 학생 성적 예제로 시작해봅시다:

import pandas as pd
 
grades = pd.DataFrame({
    'student': ['Alice', 'Bob', 'Charlie'],
    'math': [90, 78, 85],
    'science': [85, 92, 88],
    'english': [88, 80, 91]
})
 
long = pd.melt(grades, id_vars=['student'], value_vars=['math', 'science', 'english'],
               var_name='subject', value_name='score')
print(long)

Output:

   student  subject  score
0    Alice     math     90
1      Bob     math     78
2  Charlie     math     85
3    Alice  science     85
4      Bob  science     92
5  Charlie  science     88
6    Alice  english     88
7      Bob  english     80
8  Charlie  english     91

이제 각 행은 “학생-과목” 조합 하나를 나타냅니다. 원래 3x4 DataFrame(3행, 4열)이 9x3 DataFrame(9행, 3열)로 변환되었습니다.

value_vars 생략하기

value_vars를 생략하면, pandas는 id_vars에 포함되지 않은 모든 컬럼을 melt합니다:

long = grades.melt(id_vars=['student'], var_name='subject', value_name='score')
print(long)

이는 앞선 예제와 동일한 결과를 만듭니다. value_vars를 생략하는 방식은 “식별자 컬럼을 제외한 모든 컬럼을 melt하고 싶을 때” 편리합니다.

id_vars 없이 melt하기

식별자 컬럼을 전혀 지정하지 않고도 melt할 수 있습니다. 이 경우 모든 컬럼이 variable/value 쌍으로 들어갑니다:

temperatures = pd.DataFrame({
    'Jan': [30, 28],
    'Feb': [32, 31],
    'Mar': [45, 42]
})
 
long = temperatures.melt(var_name='month', value_name='temp_f')
print(long)

Output:

  month  temp_f
0   Jan      30
1   Jan      28
2   Feb      32
3   Feb      31
4   Mar      45
5   Mar      42

모든 컬럼이 측정값이고 보존할 식별자가 없는 경우에 유용합니다.

식별자 컬럼이 여러 개인 경우

실제 데이터셋에는 식별자가 하나 이상인 경우가 많습니다. id_vars에 컬럼명 리스트를 넘기면 됩니다:

sales = pd.DataFrame({
    'region': ['North', 'South', 'North', 'South'],
    'product': ['Widget', 'Widget', 'Gadget', 'Gadget'],
    'q1_revenue': [1200, 1500, 800, 950],
    'q2_revenue': [1400, 1600, 900, 1100],
    'q3_revenue': [1100, 1450, 850, 1000],
    'q4_revenue': [1500, 1700, 1000, 1200]
})
 
long_sales = sales.melt(
    id_vars=['region', 'product'],
    var_name='quarter',
    value_name='revenue'
)
print(long_sales)

Output:

    region product      quarter  revenue
0    North  Widget   q1_revenue     1200
1    South  Widget   q1_revenue     1500
2    North  Gadget   q1_revenue      800
3    South  Gadget   q1_revenue      950
4    North  Widget   q2_revenue     1400
5    South  Widget   q2_revenue     1600
6    North  Gadget   q2_revenue      900
7    South  Gadget   q2_revenue     1100
8    North  Widget   q3_revenue     1100
9    South  Widget   q3_revenue     1450
10   North  Gadget   q3_revenue      850
11   South  Gadget   q3_revenue     1000
12   North  Widget   q4_revenue     1500
13   South  Widget   q4_revenue     1700
14   North  Gadget   q4_revenue     1000
15   South  Gadget   q4_revenue     1200

regionproduct가 모든 행에 대해 보존되고, 4개의 분기 매출 컬럼이 두 컬럼으로 접힙니다.

melt 이후 데이터 정리하기

melt 이후 variable 컬럼에는 종종 정리하고 싶은 문자열이 들어있습니다. 위 예제에서 quarter 값은 Q1이 아니라 q1_revenue 같은 형태입니다. 문자열 연산으로 정리할 수 있습니다:

long_sales['quarter'] = long_sales['quarter'].str.replace('_revenue', '').str.upper()
print(long_sales.head())

Output:

  region product quarter  revenue
0  North  Widget      Q1     1200
1  South  Widget      Q1     1500
2  North  Gadget      Q1      800
3  South  Gadget      Q1      950
4  North  Widget      Q2     1400

선택한 컬럼만 melt하기

일부 컬럼만 melt하고 싶다면 value_vars에 명시적으로 지정합니다:

survey = pd.DataFrame({
    'respondent': ['R1', 'R2', 'R3'],
    'age': [25, 34, 42],
    'q1_satisfaction': [4, 5, 3],
    'q2_satisfaction': [3, 4, 5],
    'q3_satisfaction': [5, 3, 4],
    'income': [50000, 75000, 60000]
})
 
# 만족도 컬럼만 melt하고, age와 income은 식별자로 유지
long_survey = survey.melt(
    id_vars=['respondent', 'age', 'income'],
    value_vars=['q1_satisfaction', 'q2_satisfaction', 'q3_satisfaction'],
    var_name='question',
    value_name='rating'
)
print(long_survey)

Output:

  respondent  age  income         question  rating
0         R1   25   50000  q1_satisfaction       4
1         R2   34   75000  q1_satisfaction       5
2         R3   42   60000  q1_satisfaction       3
3         R1   25   50000  q2_satisfaction       3
4         R2   34   75000  q2_satisfaction       4
5         R3   42   60000  q2_satisfaction       5
6         R1   25   50000  q3_satisfaction       5
7         R2   34   75000  q3_satisfaction       3
8         R3   42   60000  q3_satisfaction       4

income은 melt 대상 컬럼이 아니지만 식별자 컬럼으로 유지됩니다.

실전 예제: 시계열 데이터

재무/거시경제 데이터는 날짜가 컬럼 헤더로 오는 wide 포맷인 경우가 많습니다. melt를 쓰면 플로팅 가능한 시계열 형태로 바뀝니다:

import pandas as pd
 
gdp = pd.DataFrame({
    'country': ['USA', 'UK', 'Germany'],
    '2020': [20.94, 2.71, 3.89],
    '2021': [23.00, 3.12, 4.26],
    '2022': [25.46, 3.07, 4.07],
    '2023': [27.36, 3.33, 4.46]
})
 
gdp_long = gdp.melt(id_vars=['country'], var_name='year', value_name='gdp_trillion_usd')
gdp_long['year'] = gdp_long['year'].astype(int)
gdp_long = gdp_long.sort_values(['country', 'year']).reset_index(drop=True)
print(gdp_long)

Output:

   country  year  gdp_trillion_usd
0  Germany  2020              3.89
1  Germany  2021              4.26
2  Germany  2022              4.07
3  Germany  2023              4.46
4       UK  2020              2.71
5       UK  2021              3.12
6       UK  2022              3.07
7       UK  2023              3.33
8      USA  2020             20.94
9      USA  2021             23.00
10     USA  2022             25.46
11     USA  2023             27.36

이제 GDP를 연도별로 쉽게 플로팅하거나, 국가별 groupby를 하거나, 전년 대비 성장률을 계산할 수 있습니다.

MultiIndex 컬럼에서 melt하기

DataFrame의 컬럼 헤더가 multi-level인 경우 col_level 파라미터로 어느 레벨을 melt할지 지정합니다:

arrays = [['score', 'score', 'attendance', 'attendance'],
          ['midterm', 'final', 'midterm', 'final']]
columns = pd.MultiIndex.from_arrays(arrays, names=['metric', 'exam'])
 
data = pd.DataFrame([[85, 90, 95, 100], [78, 82, 90, 88]],
                     index=['Alice', 'Bob'], columns=columns)
 
# 최상위 레벨을 melt
melted = data.melt(col_level=0, var_name='metric', value_name='value', ignore_index=False)
print(melted)

복잡한 multi-level 시나리오에서는 melt 전에 droplevel()로 레벨을 내리거나, 언더스코어로 레벨들을 join해서 컬럼을 평탄화(flatten)한 뒤 melt해야 할 때도 있습니다.

Melt vs Pivot: 서로 역연산

melt()pivot()은 서로 역연산입니다. melt는 wide → long, pivot은 long → wide로 변환합니다.

import pandas as pd
 
# Start wide
wide = pd.DataFrame({
    'name': ['Alice', 'Bob'],
    'math': [90, 78],
    'science': [85, 92]
})
 
# Melt: wide -> long
long = wide.melt(id_vars='name', var_name='subject', value_name='score')
print("Long format:")
print(long)
 
# Pivot: long -> wide (round-trip)
back_to_wide = long.pivot(index='name', columns='subject', values='score').reset_index()
back_to_wide.columns.name = None
print("\nBack to wide format:")
print(back_to_wide)

Output:

Long format:
    name  subject  score
0  Alice     math     90
1    Bob     math     78
2  Alice  science     85
3    Bob  science     92

Back to wide format:
    name  math  science
0  Alice    90       85
1    Bob    78       92

핵심 차이: pivot()은 index-column 조합이 유일해야 합니다. long 데이터에 중복이 있다면 집계 함수를 사용하는 pivot_table()을 대신 사용해야 합니다.

Melt vs Stack vs wide_to_long

pandas에는 여러 리쉐이핑 함수가 있습니다. 각각의 사용 시점은 다음과 같습니다:

FunctionDirectionBest ForKey Difference
melt()wide to long특정 컬럼들을 행으로 unpivot컬럼 기반; 초보자에게 가장 직관적
stack()wide to long컬럼 레벨을 인덱스 레벨로 내려서 접기인덱스 기반; MultiIndex에 강함
wide_to_long()wide to long공통 prefix + 숫자 suffix 컬럼 (예: score1, score2)stub name을 자동 파싱
pivot()long to wide값을 컬럼으로 펼치기(유일 키)melt()의 역연산
unstack()long to wide인덱스 레벨을 컬럼으로 확장stack()의 역연산

stack()을 대신 써야 하는 경우

stack()은 컬럼 인덱스를 행 인덱스로 밀어 넣습니다. 이미 MultiIndex를 쓰고 있고 인덱스 레벨에서 리쉐이프하고 싶을 때 유용합니다:

wide = pd.DataFrame({
    'math': [90, 78],
    'science': [85, 92]
}, index=['Alice', 'Bob'])
 
stacked = wide.stack()
print(stacked)

Output:

Alice  math       90
       science    85
Bob    math       78
       science     92
dtype: int64

결과는 DataFrame이 아니라 MultiIndex를 가진 Series입니다(melt()처럼 이름이 있는 컬럼을 가진 깔끔한 DataFrame이 아님). 명시적 컬럼명을 가진 평평한(flat) DataFrame 결과가 필요하면 melt()를 쓰는 편이 좋습니다.

wide_to_long()을 써야 하는 경우

wide_to_long()score1, score2, score3처럼 컬럼이 규칙적인 네이밍 패턴을 가질 때를 위해 설계되었습니다:

df = pd.DataFrame({
    'student': ['Alice', 'Bob'],
    'score1': [90, 78],
    'score2': [85, 92],
    'score3': [88, 80]
})
 
long = pd.wide_to_long(df, stubnames='score', i='student', j='exam_num')
print(long.reset_index())

Output:

  student  exam_num  score
0   Alice         1     90
1   Alice         2     85
2   Alice         3     88
3     Bob         1     78
4     Bob         2     92
5     Bob         3     80

컬럼이 일관된 prefix-suffix 패턴이면 wide_to_long()이 편하고, 그렇지 않다면 melt()가 더 유연합니다.

성능 고려사항

대부분의 데이터셋(수백만 행 이하)에서는 melt()가 충분히 빠릅니다. 100,000행 50컬럼을 melt하는 벤치마크 예시는 다음과 같습니다:

OperationApproximate Time
melt() with 50 value columns~15 ms
stack() equivalent~10 ms
Manual loop with concat()~500 ms

성능을 더 끌어올리는 팁:

  1. value_vars를 명시적으로 지정 — 필요한 컬럼만 melt하는 것이 전체를 melt하는 것보다 빠릅니다.
  2. ignore_index=True 사용(기본값) — 원래 인덱스를 보존하면 오버헤드가 생깁니다.
  3. melt 후 즉시 다시 pivot하는 왕복을 피하기 — 다른 wide 포맷이 필요하면 round-trip 대신 pivot_table()이나 rename() 등을 직접 고려하세요.
  4. 아주 큰 DataFrame(melt 후 1억+ 행)에서는 lazy evaluation과 병렬 처리가 가능한 polars나 Dask를 고려할 수 있습니다.
# 실제로 필요한 컬럼만 melt
long = df.melt(
    id_vars=['id'],
    value_vars=['col_a', 'col_b', 'col_c'],  # 50개 전체가 아니라 일부만
    var_name='metric',
    value_name='reading'
)

자주 발생하는 에러와 해결 방법

1. KeyError: Column Not Found

id_vars 또는 value_vars에 지정한 컬럼명이 DataFrame에 없을 때 발생합니다:

# Wrong: 컬럼 이름에 오타가 있음
long = df.melt(id_vars=['stduent'])  # KeyError
 
# Fix: 먼저 컬럼명을 확인
print(df.columns.tolist())

2. 기대하지 않은 중복 행이 생김

melt 자체가 중복 행을 “만드는” 것은 아닙니다. id 변수 조합마다 한 행씩 생성할 뿐입니다. 중복이 보인다면 원본 데이터에 식별자 행이 중복되어 있었을 가능성이 큽니다:

# id 컬럼 기준 중복 확인
print(df.duplicated(subset=['student']).sum())

3. value 컬럼의 dtype이 섞이는 문제

서로 다른 dtype 컬럼들을 함께 melt하면(예: int64float64 혼합) pandas는 value 컬럼을 더 일반적인 타입으로 upcast합니다. 숫자 컬럼과 문자열 컬럼을 섞어 melt하면 value 컬럼이 object dtype이 됩니다:

df = pd.DataFrame({
    'id': [1, 2],
    'score': [90, 85],
    'grade': ['A', 'B']
})
 
long = df.melt(id_vars='id')
print(long.dtypes)
# variable    object
# value       object  <-- score와 grade가 함께 들어가면서 object가 됨

이를 피하려면 숫자 컬럼과 문자열 컬럼을 분리해서 각각 melt하세요.

PyGWalker로 melt 결과 시각화하기

wide 포맷에서 long 포맷으로 데이터를 리쉐이프한 다음 자연스러운 다음 단계는 결과를 시각적으로 탐색하는 것입니다. 분포를 확인하고, 그룹을 비교하고, 이상치를 찾는 작업이죠. PyGWalker (opens in a new tab)는 어떤 pandas DataFrame이든 Jupyter Notebook 안에서 Tableau처럼 인터랙티브하게 탐색할 수 있게 해주는 오픈소스 Python 라이브러리입니다.

import pandas as pd
import pygwalker as pyg
 
# wide 데이터를 long 포맷으로 melt
grades = pd.DataFrame({
    'student': ['Alice', 'Bob', 'Charlie', 'Diana'],
    'math': [90, 78, 85, 92],
    'science': [85, 92, 88, 79],
    'english': [88, 80, 91, 84]
})
 
long = grades.melt(id_vars='student', var_name='subject', value_name='score')
 
# 인터랙티브 시각화 실행
walker = pyg.walk(long)

PyGWalker에서는 subject를 x-axis로, score를 y-axis로 드래그하고, student로 색상을 나누면 과목별 성과를 즉시 비교할 수 있습니다—차트 코드를 한 줄도 쓰지 않고도 가능합니다. bar chart, scatter plot, box plot 등도 드래그앤드롭으로 지원합니다.

Google Colab (opens in a new tab), Kaggle (opens in a new tab)에서 PyGWalker를 사용해보거나, pip install pygwalker로 설치할 수 있습니다.

FAQ

pandas melt는 무엇을 하나요?

pandas melt()는 DataFrame을 wide 포맷에서 long 포맷으로 리쉐이프합니다. 컬럼들을 행으로 바꾸면서 두 개의 새 컬럼을 만듭니다: 하나는 원래 컬럼명(변수명, variable), 다른 하나는 값(value)입니다. 이는 “unpivoting”이라고도 합니다.

pandas에서 melt와 pivot의 차이는 무엇인가요?

melt()는 wide 포맷을 long 포맷으로 변환합니다(컬럼이 행이 됨). pivot()은 그 반대로 long 포맷을 wide 포맷으로 변환합니다(행이 컬럼이 됨). 둘은 역연산 관계입니다. 같은 파라미터로 DataFrame을 melt한 뒤 결과를 pivot하면 원래 DataFrame으로 되돌릴 수 있습니다.

pandas에서 melt와 stack 중 언제 무엇을 써야 하나요?

이름이 있는 컬럼을 갖는 깔끔한 DataFrame을 원하고, 어떤 컬럼을 unpivot할지 명시적으로 제어하고 싶다면 melt()를 사용하세요. MultiIndex 컬럼을 다루고 있고, 컬럼 레벨을 행 인덱스 레벨로 내리고 싶다면 stack()을 사용하세요. melt()는 초보자에게 더 직관적이고, stack()은 계층형 리쉐이핑에서 더 강력합니다.

pandas에서 여러 컬럼을 melt하려면 어떻게 하나요?

value_vars 파라미터에 컬럼명 리스트를 전달하세요: df.melt(id_vars=['id'], value_vars=['col_a', 'col_b', 'col_c']). 지정한 컬럼들이 모두 행으로 unpivot됩니다. value_vars를 생략하면 pandas는 id_vars에 없는 모든 컬럼을 melt합니다.

컬럼명이 중복인 DataFrame도 melt할 수 있나요?

pandas는 melt할 수 있지만, 결과에서 variable 컬럼에 중복 값이 생겨 혼란스러울 수 있습니다. 모호함을 피하려면 df.columns = [...] 또는 df.rename()으로 중복 컬럼을 먼저 이름 변경하는 것이 좋습니다.

pandas에서 melt를 되돌리려면 어떻게 하나요?

pivot() 또는 pivot_table()을 사용해 melt된(long) 데이터를 다시 wide 포맷으로 바꿀 수 있습니다: long.pivot(index='id', columns='variable', values='value'). index-column 조합이 중복되어 집계가 필요하면 pivot_table()을 사용하세요.

결론

pandas melt() 함수는 Python에서 wide-format DataFrame을 long(tidy) 포맷으로 바꾸는 표준적인 방법입니다. 핵심 요약은 다음과 같습니다:

  • 식별자로 유지할 컬럼은 **id_vars**로 지정합니다.
  • melt할 컬럼은 **value_vars**로 제어합니다. 이를 생략하면 식별자 외의 모든 컬럼이 melt됩니다.
  • 결과 컬럼 이름은 **var_name, value_name**으로 의미 있게 지정하세요.
  • melt와 pivot은 역연산입니다 — wide→long은 melt(), long→wide는 pivot()입니다.
  • MultiIndex Series가 아니라 명시적인 컬럼을 가진 평평한 DataFrame이 필요하면 stack()보다 melt()가 적합합니다.
  • melt 후에는 정리(clean-up)가 자주 필요합니다 — variable 컬럼에 대해 string 메서드로 prefix/suffix 제거, 포맷 정규화를 하세요.

데이터를 리쉐이프한 뒤에는 PyGWalker (opens in a new tab) 같은 도구로 차트 코드를 쓰지 않고도 결과를 시각적으로 탐색할 수 있어 분석 워크플로우를 더 빠르고 직관적으로 만들 수 있습니다.

📚