Skip to content

Seaborn Lineplot: sns.lineplot()로 라인 차트 완벽 가이드

Updated on

라인 차트는 추세(trend), 시계열(time series) 데이터, 연속형 변수 간 관계를 시각화하는 데 필수적입니다. 하지만 인사이트를 명확하게 전달하는 출판 품질의 라인 플롯을 만들려면, 종종 matplotlib 설정과 수동 스타일링에 많은 시간이 들어갑니다. 데이터 과학자들은 분석에 집중해야 할 시간을, 라인 색을 조정하고 범례를 관리하고 축을 포맷팅하는 데 허비하곤 합니다.

Seaborn의 lineplot() 함수는 최소한의 코드로 보기 좋은 라인 차트를 만들 수 있는 고수준 인터페이스를 제공해 이 문제를 해결합니다. 통계적 집계, 신뢰구간, 컬러 팔레트, 시각적 스타일을 자동으로 처리하면서도 고급 사용 사례를 위한 높은 커스터마이징 가능성을 유지합니다.

이 가이드는 기본 라인 플롯부터 커스텀 스타일링이 들어간 고급 멀티 시리즈 시각화까지 모두 다룹니다. 데이터를 효과적으로 전달하는 프로페셔널 차트를 만드는 방법을 배우게 될 것입니다.

📚

sns.lineplot() 기본 이해하기

sns.lineplot() 함수는 pandas DataFrame 또는 배열로부터 라인 차트를 생성합니다. 기본적으로 각 x값에서 여러 관측치가 있으면 자동으로 집계(aggregation)하고, 신뢰구간(confidence interval)을 표시합니다.

import seaborn as sns
import pandas as pd
import matplotlib.pyplot as plt
 
# Create sample data
df = pd.DataFrame({
    'time': range(10),
    'value': [1, 3, 2, 5, 4, 6, 5, 7, 6, 8]
})
 
# Basic lineplot
sns.lineplot(data=df, x='time', y='value')
plt.title('Basic Line Plot')
plt.show()

이 코드는 컬럼 이름에서 자동으로 축 라벨을 가져와 깔끔한 라인 차트를 만듭니다. 수동 설정 없이도 데이터 포맷팅, 스케일링, 스타일링을 처리합니다.

핵심 파라미터와 문법

sns.lineplot() 함수는 다양한 입력 포맷을 지원하며 커스터마이징 옵션도 풍부합니다.

sns.lineplot(
    data=None,      # DataFrame, array, or dict
    x=None,         # Column name or vector for x-axis
    y=None,         # Column name or vector for y-axis
    hue=None,       # Grouping variable for color
    size=None,      # Grouping variable for line width
    style=None,     # Grouping variable for line style
    palette=None,   # Color palette
    markers=False,  # Add markers to data points
    dashes=True,    # Use dashed lines for styles
    ci=95,          # Confidence interval (deprecated in newer versions)
    errorbar=('ci', 95),  # Error representation
    legend='auto',  # Legend display
    ax=None         # Matplotlib axes object
)

DataFrame을 사용할 때는 x, y 파라미터에 컬럼명을 지정하는 방식이 코드 가독성이 좋습니다.

# Create multi-observation dataset
data = pd.DataFrame({
    'day': [1, 2, 3, 1, 2, 3, 1, 2, 3],
    'sales': [100, 150, 120, 110, 145, 125, 105, 155, 130],
    'store': ['A', 'A', 'A', 'B', 'B', 'B', 'C', 'C', 'C']
})
 
# Plot with automatic aggregation
sns.lineplot(data=data, x='day', y='sales')
plt.title('Average Sales by Day (with 95% CI)')
plt.xlabel('Day of Week')
plt.ylabel('Sales ($)')
plt.show()

함수는 요일(day)별 평균 매출(mean)을 자동으로 계산하고, 라인 주변에 음영 밴드로 신뢰구간을 표시합니다.

Hue로 여러 라인 그리기

hue 파라미터를 사용하면 데이터의 그룹별로 별도의 라인을 만들고, 서로 다른 색을 자동으로 할당합니다.

# Multiple stores on same plot
fig, ax = plt.subplots(figsize=(10, 6))
 
sns.lineplot(
    data=data,
    x='day',
    y='sales',
    hue='store',  # Separate line for each store
    palette='Set2'
)
 
plt.title('Sales Comparison Across Stores')
plt.xlabel('Day of Week')
plt.ylabel('Sales ($)')
plt.legend(title='Store', loc='upper left')
plt.show()

이는 자동 범례와 함께 서로 다른 색의 3개 라인을 생성합니다. palette는 색상 스킴을 제어합니다.

더 복잡한 그룹핑의 예시는 다음과 같습니다.

# Multiple grouping variables
customer_data = pd.DataFrame({
    'month': [1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3],
    'revenue': [5000, 5500, 6000, 4800, 5200, 5800, 6200, 6800, 7200, 6000, 6500, 7000],
    'segment': ['Premium', 'Premium', 'Premium', 'Standard', 'Standard', 'Standard',
                'Premium', 'Premium', 'Premium', 'Standard', 'Standard', 'Standard'],
    'region': ['North', 'North', 'North', 'North', 'North', 'North',
               'South', 'South', 'South', 'South', 'South', 'South']
})
 
# Use hue for segment, style for region
sns.lineplot(
    data=customer_data,
    x='month',
    y='revenue',
    hue='segment',
    style='region',
    markers=True,
    dashes=False
)
 
plt.title('Revenue by Segment and Region')
plt.xlabel('Month')
plt.ylabel('Revenue ($)')
plt.show()

Style 파라미터로 라인 스타일 커스터마이징

style 파라미터는 대시 패턴(점선 등)이나 라인 타입을 바꿔 그룹별 라인 외형을 다르게 합니다.

# Temperature data with different sensors
temp_data = pd.DataFrame({
    'hour': list(range(24)) * 3,
    'temperature': [15, 14, 13, 12, 12, 13, 15, 17, 19, 21, 23, 24,
                   25, 26, 25, 24, 22, 20, 18, 17, 16, 15, 15, 14] * 3,
    'sensor': ['Sensor_A'] * 24 + ['Sensor_B'] * 24 + ['Sensor_C'] * 24,
    'location': ['Indoor'] * 24 + ['Outdoor'] * 24 + ['Basement'] * 24
})
 
# Add some variation
import numpy as np
np.random.seed(42)
temp_data['temperature'] = temp_data['temperature'] + np.random.normal(0, 1, len(temp_data))
 
fig, ax = plt.subplots(figsize=(12, 6))
 
sns.lineplot(
    data=temp_data,
    x='hour',
    y='temperature',
    hue='location',
    style='location',
    markers=True,
    dashes=True,
    palette='tab10'
)
 
plt.title('24-Hour Temperature Monitoring')
plt.xlabel('Hour of Day')
plt.ylabel('Temperature (°C)')
plt.legend(title='Location')
plt.grid(True, alpha=0.3)
plt.show()

서로 다른 라인 스타일은 흑백 출력 환경이나 접근성(색각 이상 등)을 고려해야 할 때 특히 유용합니다.

데이터 포인트에 마커 추가하기

마커는 라인을 따라 개별 데이터 포인트를 강조합니다. 데이터가 듬성듬성한 경우나 특정 관측치를 강조하고 싶을 때 유용합니다.

# Quarterly earnings data
earnings = pd.DataFrame({
    'quarter': ['Q1', 'Q2', 'Q3', 'Q4'] * 3,
    'earnings': [2.1, 2.3, 2.5, 2.4, 2.2, 2.4, 2.6, 2.7, 2.3, 2.5, 2.8, 2.9],
    'year': ['2023'] * 4 + ['2024'] * 4 + ['2025'] * 4
})
 
sns.lineplot(
    data=earnings,
    x='quarter',
    y='earnings',
    hue='year',
    markers=True,
    marker='o',  # Specific marker style
    markersize=8,
    linewidth=2.5,
    palette='deep'
)
 
plt.title('Quarterly Earnings per Share')
plt.xlabel('Quarter')
plt.ylabel('EPS ($)')
plt.ylim(2.0, 3.0)
plt.legend(title='Year')
plt.grid(True, alpha=0.3)
plt.show()

그룹별로 마커 스타일을 다르게 지정할 수도 있습니다.

# Custom markers for different groups
sns.lineplot(
    data=earnings,
    x='quarter',
    y='earnings',
    hue='year',
    style='year',
    markers=['o', 's', '^'],  # Different marker per year
    markersize=10,
    dashes=False
)
 
plt.title('Earnings Trend with Distinct Markers')
plt.show()

신뢰구간과 에러 밴드 다루기

Seaborn은 각 x값에 대해 여러 관측치가 존재할 때 신뢰구간을 자동으로 계산합니다. 이러한 통계적 집계는 데이터의 불확실성을 함께 전달하는 데 도움이 됩니다.

# Experimental data with replicates
experiment = pd.DataFrame({
    'concentration': [0.1, 0.5, 1.0, 2.0, 5.0] * 10,
    'response': np.random.lognormal(mean=[1, 1.5, 2, 2.5, 3] * 10, sigma=0.3),
    'replicate': list(range(10)) * 5
})
 
fig, axes = plt.subplots(1, 3, figsize=(15, 4))
 
# 95% confidence interval (default)
sns.lineplot(data=experiment, x='concentration', y='response',
             errorbar=('ci', 95), ax=axes[0])
axes[0].set_title('95% Confidence Interval')
 
# Standard deviation
sns.lineplot(data=experiment, x='concentration', y='response',
             errorbar='sd', ax=axes[1])
axes[1].set_title('Standard Deviation')
 
# No error bars
sns.lineplot(data=experiment, x='concentration', y='response',
             errorbar=None, ax=axes[2])
axes[2].set_title('No Error Bars')
 
plt.tight_layout()
plt.show()

errorbar 파라미터는 다양한 표현을 지원합니다.

  • ('ci', 95): 95% 신뢰구간
  • ('pi', 95): 95% 예측구간(prediction interval)
  • 'sd': 표준편차
  • 'se': 표준오차
  • ('pi', 50): 사분위 범위(IQR)
  • None: 에러 표시 없음

팔레트로 색상 커스터마이징

palette 파라미터는 이름이 있는 팔레트 또는 커스텀 컬러 리스트/매핑으로 라인 색을 제어합니다.

# Stock price comparison
stocks = pd.DataFrame({
    'date': pd.date_range('2025-01-01', periods=60),
    'price': np.random.randn(60).cumsum() + 100,
    'ticker': ['AAPL'] * 60
})
 
stocks = pd.concat([
    stocks,
    pd.DataFrame({
        'date': pd.date_range('2025-01-01', periods=60),
        'price': np.random.randn(60).cumsum() + 150,
        'ticker': ['GOOGL'] * 60
    }),
    pd.DataFrame({
        'date': pd.date_range('2025-01-01', periods=60),
        'price': np.random.randn(60).cumsum() + 200,
        'ticker': ['MSFT'] * 60
    })
])
 
fig, axes = plt.subplots(2, 2, figsize=(14, 10))
 
# Built-in palette
sns.lineplot(data=stocks, x='date', y='price', hue='ticker',
             palette='Set1', ax=axes[0, 0])
axes[0, 0].set_title('Set1 Palette')
 
# Custom colors
custom_colors = {'AAPL': '#FF6B6B', 'GOOGL': '#4ECDC4', 'MSFT': '#45B7D1'}
sns.lineplot(data=stocks, x='date', y='price', hue='ticker',
             palette=custom_colors, ax=axes[0, 1])
axes[0, 1].set_title('Custom Color Mapping')
 
# Colorblind-safe palette
sns.lineplot(data=stocks, x='date', y='price', hue='ticker',
             palette='colorblind', ax=axes[1, 0])
axes[1, 0].set_title('Colorblind Palette')
 
# Dark palette
sns.lineplot(data=stocks, x='date', y='price', hue='ticker',
             palette='dark', ax=axes[1, 1])
axes[1, 1].set_title('Dark Palette')
 
plt.tight_layout()
plt.show()

자주 쓰는 팔레트 옵션:

  • 'deep', 'muted', 'pastel', 'bright', 'dark', 'colorblind'
  • 'Set1', 'Set2', 'Set3', 'Paired', 'tab10'
  • 'viridis', 'plasma', 'inferno', 'magma', 'cividis'

시계열(Time Series) 시각화

Seaborn lineplot은 날짜 포맷을 자동 처리해 시계열 시각화에 특히 강합니다.

# Website traffic data
dates = pd.date_range('2025-01-01', periods=180, freq='D')
traffic = pd.DataFrame({
    'date': dates,
    'visits': 1000 + np.random.randn(180).cumsum() * 50 + np.sin(np.arange(180) / 7) * 200,
    'source': ['Organic'] * 180
})
 
# Add paid traffic
paid = pd.DataFrame({
    'date': dates,
    'visits': 500 + np.random.randn(180).cumsum() * 30,
    'source': ['Paid'] * 180
})
 
traffic = pd.concat([traffic, paid])
 
fig, ax = plt.subplots(figsize=(14, 6))
 
sns.lineplot(
    data=traffic,
    x='date',
    y='visits',
    hue='source',
    palette={'Organic': '#2ecc71', 'Paid': '#e74c3c'},
    linewidth=2
)
 
plt.title('Website Traffic Over Time', fontsize=16, fontweight='bold')
plt.xlabel('Date', fontsize=12)
plt.ylabel('Daily Visits', fontsize=12)
plt.legend(title='Traffic Source', title_fontsize=11, fontsize=10)
plt.grid(True, alpha=0.3, linestyle='--')
 
# Format x-axis dates
import matplotlib.dates as mdates
ax.xaxis.set_major_formatter(mdates.DateFormatter('%b %Y'))
ax.xaxis.set_major_locator(mdates.MonthLocator(interval=1))
plt.xticks(rotation=45)
 
plt.tight_layout()
plt.show()

datetime index를 가진 데이터의 경우:

# Create time series with datetime index
ts_data = pd.DataFrame({
    'value': np.random.randn(365).cumsum() + 50
}, index=pd.date_range('2025-01-01', periods=365))
 
# Reset index to use in lineplot
ts_data_reset = ts_data.reset_index()
ts_data_reset.columns = ['date', 'value']
 
sns.lineplot(data=ts_data_reset, x='date', y='value')
plt.title('Time Series with Datetime Index')
plt.xticks(rotation=45)
plt.tight_layout()
plt.show()

Wide-format 데이터에서 여러 라인 그리기

Wide-format DataFrame은 서로 다른 변수를 별도의 컬럼에 저장합니다. Seaborn은 이를 직접 플롯할 수도 있습니다.

# Wide format data
wide_data = pd.DataFrame({
    'month': range(1, 13),
    'Product_A': [100, 120, 115, 130, 140, 135, 150, 160, 155, 170, 180, 175],
    'Product_B': [80, 85, 90, 95, 100, 105, 110, 115, 120, 125, 130, 135],
    'Product_C': [60, 65, 70, 68, 75, 80, 85, 90, 88, 95, 100, 105]
})
 
# Method 1: Melt to long format
long_data = wide_data.melt(id_vars='month', var_name='Product', value_name='Sales')
 
sns.lineplot(data=long_data, x='month', y='Sales', hue='Product')
plt.title('Product Sales Comparison')
plt.xlabel('Month')
plt.ylabel('Sales')
plt.show()

또는 wide format에서 각 컬럼을 따로 그리는 방식도 가능합니다.

# Method 2: Plot each column separately
fig, ax = plt.subplots(figsize=(10, 6))
 
for column in ['Product_A', 'Product_B', 'Product_C']:
    sns.lineplot(data=wide_data, x='month', y=column, label=column, ax=ax)
 
plt.title('Product Sales (Wide Format)')
plt.xlabel('Month')
plt.ylabel('Sales')
plt.legend(title='Product')
plt.show()

Matplotlib와 결합해 고급 커스터마이징하기

Seaborn lineplot은 matplotlib과 자연스럽게 통합되어 세밀한 제어가 가능합니다.

# Create figure with custom styling
fig, ax = plt.subplots(figsize=(12, 7))
 
# Set overall style
sns.set_style('whitegrid')
sns.set_context('notebook', font_scale=1.2)
 
# Plot data
performance = pd.DataFrame({
    'epoch': list(range(1, 51)) * 2,
    'accuracy': np.concatenate([
        0.6 + 0.008 * np.arange(50) + np.random.randn(50) * 0.02,
        0.55 + 0.009 * np.arange(50) + np.random.randn(50) * 0.025
    ]),
    'model': ['Model_A'] * 50 + ['Model_B'] * 50
})
 
sns.lineplot(
    data=performance,
    x='epoch',
    y='accuracy',
    hue='model',
    palette=['#FF6B6B', '#4ECDC4'],
    linewidth=2.5,
    ax=ax
)
 
# Customize with matplotlib
ax.set_title('Model Training Performance', fontsize=18, fontweight='bold', pad=20)
ax.set_xlabel('Training Epoch', fontsize=14, fontweight='bold')
ax.set_ylabel('Validation Accuracy', fontsize=14, fontweight='bold')
ax.set_ylim(0.5, 1.0)
ax.yaxis.set_major_formatter(plt.FuncFormatter(lambda y, _: f'{y:.0%}'))
 
# Add reference line
ax.axhline(y=0.8, color='gray', linestyle='--', linewidth=1.5, alpha=0.7, label='Target (80%)')
 
# Customize legend
handles, labels = ax.get_legend_handles_labels()
ax.legend(handles, labels, title='Model', title_fontsize=12,
          fontsize=11, loc='lower right', framealpha=0.95)
 
# Add grid customization
ax.grid(True, alpha=0.4, linestyle=':', linewidth=0.8)
ax.set_facecolor('#F8F9FA')
 
# Add annotations
ax.annotate('Model A reaches 90%',
            xy=(45, 0.9), xytext=(35, 0.85),
            arrowprops=dict(arrowstyle='->', color='#FF6B6B', lw=1.5),
            fontsize=10, color='#FF6B6B', fontweight='bold')
 
plt.tight_layout()
plt.show()

여러 lineplot을 Subplot으로 만들기

서브플롯 레이아웃을 사용하면 데이터의 여러 측면을 비교할 수 있습니다.

# Multi-metric dashboard
metrics = pd.DataFrame({
    'time': list(range(100)) * 3,
    'cpu_usage': np.random.rand(300) * 60 + 20 + np.sin(np.arange(300) / 10) * 15,
    'memory_usage': np.random.rand(300) * 40 + 40 + np.cos(np.arange(300) / 15) * 10,
    'disk_io': np.random.rand(300) * 80 + 10 + np.sin(np.arange(300) / 8) * 20,
    'server': ['Server_1'] * 100 + ['Server_2'] * 100 + ['Server_3'] * 100
})
 
fig, axes = plt.subplots(3, 1, figsize=(12, 10), sharex=True)
 
# CPU Usage
sns.lineplot(data=metrics, x='time', y='cpu_usage', hue='server',
             palette='Set2', ax=axes[0], legend=True)
axes[0].set_title('CPU Usage (%)', fontsize=14, fontweight='bold')
axes[0].set_ylabel('Usage (%)')
axes[0].set_xlabel('')
axes[0].axhline(y=80, color='red', linestyle='--', alpha=0.5, label='Threshold')
axes[0].legend(loc='upper left', ncol=4)
 
# Memory Usage
sns.lineplot(data=metrics, x='time', y='memory_usage', hue='server',
             palette='Set2', ax=axes[1], legend=False)
axes[1].set_title('Memory Usage (%)', fontsize=14, fontweight='bold')
axes[1].set_ylabel('Usage (%)')
axes[1].set_xlabel('')
axes[1].axhline(y=80, color='red', linestyle='--', alpha=0.5)
 
# Disk I/O
sns.lineplot(data=metrics, x='time', y='disk_io', hue='server',
             palette='Set2', ax=axes[2], legend=False)
axes[2].set_title('Disk I/O (MB/s)', fontsize=14, fontweight='bold')
axes[2].set_ylabel('Throughput')
axes[2].set_xlabel('Time (seconds)', fontsize=12)
 
plt.tight_layout()
plt.show()

비교 분석을 위한 그리드 레이아웃:

# 2x2 comparison grid
fig, axes = plt.subplots(2, 2, figsize=(14, 10))
 
categories = ['Category_A', 'Category_B', 'Category_C', 'Category_D']
positions = [(0, 0), (0, 1), (1, 0), (1, 1)]
 
for category, (i, j) in zip(categories, positions):
    subset = pd.DataFrame({
        'x': range(20),
        'y': np.random.randn(20).cumsum() + 10
    })
 
    sns.lineplot(data=subset, x='x', y='y', ax=axes[i, j],
                marker='o', linewidth=2, color='#3498db')
    axes[i, j].set_title(f'{category} Trend', fontweight='bold')
    axes[i, j].grid(True, alpha=0.3)
 
plt.suptitle('Multi-Category Performance Dashboard', fontsize=16, fontweight='bold', y=1.02)
plt.tight_layout()
plt.show()

비교 표: sns.lineplot vs plt.plot vs sns.relplot

Featuresns.lineplot()plt.plot()sns.relplot(kind="line")
Automatic aggregationYes (mean + CI)NoYes (mean + CI)
DataFrame integrationNative supportRequires array conversionNative support
Multiple groups (hue)Automatic coloringManual iterationAutomatic coloring
Confidence intervalsBuilt-inManual calculationBuilt-in
FacetGrid supportNo (single axes)NoYes (automatic subplots)
Statistical estimationMean, median, etc.NoneMean, median, etc.
Semantic mappingshue, size, styleManualhue, size, style, col, row
Default stylingSeaborn themeMatplotlib defaultSeaborn theme
Legend handlingAutomaticManualAutomatic
Code complexityLowMediumLow
Performance (large data)MediumFastMedium
Customization depthHigh (via ax)HighestMedium (FacetGrid limits)
Best use caseSingle plot, grouped dataSimple plots, full controlMulti-facet comparisons

sns.lineplot()을 사용하세요: 단일 플롯에서 자동 집계와 통계적 시각화가 필요할 때.

plt.plot()을 사용하세요: 최대한의 제어, 성능, 또는 이미 집계된 데이터(pre-aggregated data)를 그릴 때.

sns.relplot(kind="line")을 사용하세요: 자동 서브플롯 생성이 포함된 facet(패싯) 플롯이 필요할 때.

파라미터 레퍼런스 표

ParameterTypeDefaultDescription
dataDataFrame, dict, arrayNone입력 데이터 구조
x, ystr, arrayNonex축과 y축에 사용할 변수
huestrNone색상 인코딩을 위한 그룹 변수
sizestrNone라인 두께를 위한 그룹 변수
stylestrNone라인 대시 패턴을 위한 그룹 변수
palettestr, list, dictNonehue 레벨을 위한 컬러 팔레트
hue_orderlistNonehue 변수 레벨의 순서
unitsstrNone샘플링 유닛 그룹핑(집계 없음)
estimatorfunctionnp.mean집계 함수(mean, median 등)
errorbartuple, str('ci', 95)에러 표현 방식
n_bootint1000CI 부트스트랩 반복 횟수
seedintNone부트스트랩 랜덤 시드
sortboolTrue플로팅 전에 x 변수를 정렬할지 여부
err_stylestr'band'에러 표시 스타일: 'band' 또는 'bars'
err_kwsdictNone에러 표현을 위한 키워드 인자
markersbool, listFalse데이터 포인트 마커 스타일
dashesbool, listTrue라인 대시 패턴
legendstr, bool'auto'범례 표시 동작
ciint, 'sd', NoneDeprecated대신 errorbar 사용
axAxesNonematplotlib axes 객체
linewidthfloat1.5라인 두께
linestylestr'-'라인 스타일 ('-', '--', '-.', ':')

실전 예시: 주가(Stock Price) 분석

# Simulate stock price data
np.random.seed(42)
dates = pd.date_range('2024-01-01', periods=252, freq='B')  # Business days
 
stocks_real = pd.DataFrame({
    'Date': np.tile(dates, 4),
    'Price': np.concatenate([
        100 * np.exp(np.random.randn(252).cumsum() * 0.01),  # AAPL
        150 * np.exp(np.random.randn(252).cumsum() * 0.012), # GOOGL
        200 * np.exp(np.random.randn(252).cumsum() * 0.011), # MSFT
        80 * np.exp(np.random.randn(252).cumsum() * 0.015)   # TSLA
    ]),
    'Ticker': ['AAPL'] * 252 + ['GOOGL'] * 252 + ['MSFT'] * 252 + ['TSLA'] * 252
})
 
# Calculate normalized returns (base = 100)
stocks_normalized = stocks_real.copy()
for ticker in stocks_normalized['Ticker'].unique():
    mask = stocks_normalized['Ticker'] == ticker
    first_price = stocks_normalized.loc[mask, 'Price'].iloc[0]
    stocks_normalized.loc[mask, 'Normalized_Return'] = (
        stocks_normalized.loc[mask, 'Price'] / first_price * 100
    )
 
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(14, 10))
 
# Absolute prices
sns.lineplot(
    data=stocks_real,
    x='Date',
    y='Price',
    hue='Ticker',
    palette='deep',
    linewidth=2,
    ax=ax1
)
 
ax1.set_title('Stock Prices - Absolute Values (2024)', fontsize=16, fontweight='bold')
ax1.set_xlabel('')
ax1.set_ylabel('Price ($)', fontsize=12)
ax1.legend(title='Ticker', title_fontsize=11, fontsize=10, loc='upper left')
ax1.grid(True, alpha=0.3)
 
# Normalized returns
sns.lineplot(
    data=stocks_normalized,
    x='Date',
    y='Normalized_Return',
    hue='Ticker',
    palette='deep',
    linewidth=2,
    ax=ax2
)
 
ax2.axhline(y=100, color='gray', linestyle='--', linewidth=1, alpha=0.7)
ax2.set_title('Normalized Returns (Base = 100)', fontsize=16, fontweight='bold')
ax2.set_xlabel('Date', fontsize=12)
ax2.set_ylabel('Normalized Return', fontsize=12)
ax2.legend(title='Ticker', title_fontsize=11, fontsize=10, loc='upper left')
ax2.grid(True, alpha=0.3)
 
plt.tight_layout()
plt.show()

실전 예시: 센서 데이터 모니터링

# IoT sensor data with noise
hours = np.linspace(0, 24, 288)  # 5-minute intervals
sensor_data = pd.DataFrame({
    'time': np.tile(hours, 4),
    'temperature': np.concatenate([
        20 + 5 * np.sin(hours * np.pi / 12) + np.random.randn(288) * 0.5,  # Room 1
        22 + 4 * np.sin(hours * np.pi / 12 - 0.5) + np.random.randn(288) * 0.7,  # Room 2
        19 + 6 * np.sin(hours * np.pi / 12 + 0.3) + np.random.randn(288) * 0.6,  # Room 3
        21 + 5.5 * np.sin(hours * np.pi / 12 - 0.2) + np.random.randn(288) * 0.8   # Room 4
    ]),
    'room': ['Room_1'] * 288 + ['Room_2'] * 288 + ['Room_3'] * 288 + ['Room_4'] * 288,
    'building': ['Building_A'] * 576 + ['Building_B'] * 576
})
 
fig, ax = plt.subplots(figsize=(14, 7))
 
sns.lineplot(
    data=sensor_data,
    x='time',
    y='temperature',
    hue='room',
    style='building',
    palette='tab10',
    linewidth=2,
    markers=False,
    errorbar=('ci', 68),  # 1 standard deviation
    ax=ax
)
 
# Add comfort zone
ax.axhspan(18, 24, alpha=0.1, color='green', label='Comfort Zone')
 
ax.set_title('24-Hour Temperature Monitoring Across Rooms', fontsize=16, fontweight='bold')
ax.set_xlabel('Hour of Day', fontsize=12)
ax.set_ylabel('Temperature (°C)', fontsize=12)
ax.set_xticks(range(0, 25, 2))
ax.legend(title='Location', bbox_to_anchor=(1.05, 1), loc='upper left')
ax.grid(True, alpha=0.3, linestyle=':')
 
plt.tight_layout()
plt.show()

실전 예시: 시간에 따른 A/B 테스트 결과

# A/B test conversion rates over time
days = np.arange(1, 31)
ab_test = pd.DataFrame({
    'day': np.tile(days, 2),
    'conversion_rate': np.concatenate([
        0.05 + 0.001 * days + np.random.randn(30) * 0.005,  # Control
        0.055 + 0.0012 * days + np.random.randn(30) * 0.005  # Variant
    ]) * 100,
    'variant': ['Control'] * 30 + ['Variant_B'] * 30,
    'sample_size': np.random.randint(800, 1200, 60)
})
 
fig, axes = plt.subplots(2, 1, figsize=(12, 9), sharex=True)
 
# Conversion rate trend
sns.lineplot(
    data=ab_test,
    x='day',
    y='conversion_rate',
    hue='variant',
    palette={'Control': '#95a5a6', 'Variant_B': '#27ae60'},
    linewidth=2.5,
    markers=True,
    markersize=6,
    errorbar=None,
    ax=axes[0]
)
 
axes[0].set_title('A/B Test: Conversion Rate Over Time', fontsize=16, fontweight='bold')
axes[0].set_ylabel('Conversion Rate (%)', fontsize=12)
axes[0].set_xlabel('')
axes[0].legend(title='Test Group', fontsize=11)
axes[0].grid(True, alpha=0.3)
 
# Sample size tracking
sns.lineplot(
    data=ab_test,
    x='day',
    y='sample_size',
    hue='variant',
    palette={'Control': '#95a5a6', 'Variant_B': '#27ae60'},
    linewidth=2,
    markers=False,
    errorbar=None,
    ax=axes[1]
)
 
axes[1].set_title('Daily Sample Size', fontsize=14, fontweight='bold')
axes[1].set_ylabel('Users', fontsize=12)
axes[1].set_xlabel('Day of Test', fontsize=12)
axes[1].legend(title='Test Group', fontsize=11)
axes[1].grid(True, alpha=0.3)
 
plt.tight_layout()
plt.show()

라인 차트 저장 및 내보내기

리포트나 프레젠테이션을 위해 다양한 포맷으로 저장할 수 있습니다.

# Create a polished chart for export
fig, ax = plt.subplots(figsize=(10, 6), dpi=100)
 
export_data = pd.DataFrame({
    'month': ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'] * 2,
    'revenue': [50, 55, 53, 60, 65, 70, 45, 48, 50, 55, 58, 63],
    'region': ['North'] * 6 + ['South'] * 6
})
 
sns.lineplot(
    data=export_data,
    x='month',
    y='revenue',
    hue='region',
    palette='Set1',
    linewidth=3,
    markers=True,
    markersize=10,
    ax=ax
)
 
ax.set_title('Regional Revenue Comparison', fontsize=16, fontweight='bold', pad=15)
ax.set_xlabel('Month', fontsize=13)
ax.set_ylabel('Revenue ($K)', fontsize=13)
ax.legend(title='Region', title_fontsize=12, fontsize=11)
ax.grid(True, alpha=0.4)
 
# Save in multiple formats
plt.savefig('revenue_comparison.png', dpi=300, bbox_inches='tight')  # High-res PNG
plt.savefig('revenue_comparison.pdf', bbox_inches='tight')           # Vector PDF
plt.savefig('revenue_comparison.svg', bbox_inches='tight')           # SVG for web
 
# Save with transparent background
plt.savefig('revenue_comparison_transparent.png', dpi=300,
            bbox_inches='tight', transparent=True)
 
plt.show()
 
print("Charts saved in multiple formats:")
print("- revenue_comparison.png (300 DPI)")
print("- revenue_comparison.pdf (vector)")
print("- revenue_comparison.svg (web)")
print("- revenue_comparison_transparent.png (transparent)")

특정 크기(치수)로 저장하기:

# Set exact figure size for publication
fig = plt.figure(figsize=(8, 5))  # Width, height in inches
ax = fig.add_subplot(111)
 
sns.lineplot(data=export_data, x='month', y='revenue', hue='region', ax=ax)
ax.set_title('Revenue Trends')
 
# Save with exact pixel dimensions (at 100 DPI: 8 inches * 100 = 800 pixels)
plt.savefig('chart_800x500.png', dpi=100, bbox_inches='tight')
 
# Save with higher resolution (8 inches * 300 DPI = 2400 pixels)
plt.savefig('chart_2400x1500.png', dpi=300, bbox_inches='tight')
 
plt.close()

PyGWalker로 인터랙티브 라인 차트 만들기

라인 차트 데이터를 인터랙티브하게 탐색하려면, DataFrame을 Tableau 같은 인터랙티브 시각화로 변환해주는 오픈소스 Python 라이브러리 PyGWalker를 고려해볼 수 있습니다.

# Install PyGWalker: pip install pygwalker
 
import pygwalker as pyg
import pandas as pd
import numpy as np
 
# Create time series data
dates = pd.date_range('2025-01-01', periods=365, freq='D')
interactive_data = pd.DataFrame({
    'Date': dates,
    'Sales': 1000 + np.random.randn(365).cumsum() * 50,
    'Costs': 600 + np.random.randn(365).cumsum() * 30,
    'Region': np.random.choice(['North', 'South', 'East', 'West'], 365),
    'Product': np.random.choice(['Product_A', 'Product_B', 'Product_C'], 365)
})
 
# Launch interactive explorer
walker = pyg.walk(interactive_data)

PyGWalker가 제공하는 기능:

  • 드래그 앤 드롭으로 라인 차트 생성
  • 인터랙티브 필터링 및 그룹핑
  • 자동 집계 및 날짜 binning
  • 멀티 시리즈 비교
  • 정적 차트로 내보내기(export)
  • 시각화마다 수동 코딩이 필요 없음

이는 특히 탐색적 데이터 분석(EDA)에서, 다양한 그룹/기간/집계를 코드로 매번 작성하지 않고 빠르게 실험해야 할 때 유용합니다. 설치 및 문서는 github.com/Kanaries/pygwalker (opens in a new tab)를 참고하세요.

고급 팁과 베스트 프랙티스

대용량 데이터셋 처리

시리즈당 수천 개 이상의 포인트가 있는 데이터셋이라면 다운샘플링 또는 집계를 고려하세요.

# Large dataset simulation
large_data = pd.DataFrame({
    'timestamp': pd.date_range('2025-01-01', periods=10000, freq='T'),
    'value': np.random.randn(10000).cumsum()
})
 
# Method 1: Downsample to hourly
hourly = large_data.set_index('timestamp').resample('H').mean().reset_index()
 
sns.lineplot(data=hourly, x='timestamp', y='value')
plt.title('Downsampled to Hourly Average')
plt.show()
 
# Method 2: Use estimator for automatic aggregation
large_data['hour'] = large_data['timestamp'].dt.floor('H')
 
sns.lineplot(data=large_data, x='hour', y='value', estimator='median')
plt.title('Median Aggregation by Hour')
plt.xticks(rotation=45)
plt.tight_layout()
plt.show()

여러 Estimator 결합하기

서로 다른 통계적 집계를 비교해보세요.

# Noisy experimental data
experiment_multi = pd.DataFrame({
    'dose': [0.1, 0.5, 1.0, 2.0, 5.0] * 20,
    'response': np.random.lognormal([1, 1.5, 2, 2.5, 3] * 20, 0.4)
})
 
fig, axes = plt.subplots(1, 3, figsize=(15, 4))
 
# Mean with CI
sns.lineplot(data=experiment_multi, x='dose', y='response',
             estimator='mean', errorbar=('ci', 95), ax=axes[0])
axes[0].set_title('Mean ± 95% CI')
 
# Median with IQR
sns.lineplot(data=experiment_multi, x='dose', y='response',
             estimator='median', errorbar=('pi', 50), ax=axes[1])
axes[1].set_title('Median ± IQR')
 
# Custom estimator (75th percentile)
sns.lineplot(data=experiment_multi, x='dose', y='response',
             estimator=lambda x: np.percentile(x, 75), errorbar=None, ax=axes[2])
axes[2].set_title('75th Percentile')
 
plt.tight_layout()
plt.show()

특정 구간 하이라이팅하기

중요한 기간에 시선을 집중시키세요.

# Sales data with promotion period
sales_highlight = pd.DataFrame({
    'week': range(1, 53),
    'sales': 1000 + np.random.randn(52).cumsum() * 100 +
             np.where((np.arange(52) >= 20) & (np.arange(52) <= 30), 500, 0)
})
 
fig, ax = plt.subplots(figsize=(12, 6))
 
sns.lineplot(data=sales_highlight, x='week', y='sales', linewidth=2.5, color='#3498db')
 
# Highlight promotion period
ax.axvspan(20, 30, alpha=0.2, color='gold', label='Promotion Period')
ax.axhline(y=sales_highlight['sales'].mean(), color='red',
           linestyle='--', linewidth=1.5, alpha=0.7, label='Average Sales')
 
ax.set_title('Sales Performance with Promotion Period Highlighted',
             fontsize=16, fontweight='bold')
ax.set_xlabel('Week', fontsize=12)
ax.set_ylabel('Sales ($)', fontsize=12)
ax.legend(fontsize=11)
ax.grid(True, alpha=0.3)
 
plt.tight_layout()
plt.show()

FAQ

결론

Seaborn의 lineplot() 함수는 출판 품질의 라인 차트를 만들기 위한 강력하면서도 접근하기 쉬운 인터페이스를 제공합니다. 기본적인 추세 시각화부터 통계적 집계를 포함한 복잡한 멀티 시리즈 비교까지, 최소한의 코드로 흔한 플로팅 작업을 처리하면서도 matplotlib 연동을 통해 완전한 커스터마이징을 유지합니다.

신뢰구간의 자동 처리, 의미 기반 색상 매핑(semantic color mapping), DataFrame 네이티브 통합 덕분에 보일러플레이트 코드가 크게 줄고, 플로팅 메커니즘보다 데이터 인사이트에 집중할 수 있습니다. 시계열 추세를 시각화하든, 실험 그룹을 비교하든, 시스템 메트릭을 모니터링하든 sns.lineplot()은 깔끔하고 정보 밀도가 높은 시각화를 제공합니다.

정적인 출판용 차트라면 seaborn의 고수준 인터페이스에 matplotlib의 세밀한 제어를 결합하세요. 인터랙티브 탐색과 빠른 프로토타이핑이 목적이라면, PyGWalker 같은 도구가 드래그 앤 드롭 UI로 이러한 역량을 확장해주며, 시각화 반복마다 코드를 작성할 필요가 없습니다.

이 기법들을 익혀, 원시 데이터를 의사결정을 이끌고 기술/비기술 청중 모두에게 인사이트를 전달하는 명확한 시각적 스토리로 바꿔보세요.

📚