Skip to content

Sklearn 선형 회귀: Python 예제로 배우는 완전 가이드

Updated on

특성(feature)과 연속형 타깃 변수가 있는 데이터셋이 있다고 해봅시다. 주택 가격, 매출, 온도 추세 같은 결과를 예측하고 싶지만 어떤 접근을 써야 하는지, 그리고 Python에서 어떻게 올바르게 설정해야 하는지 확신이 없을 수 있습니다. 잘못된 모델 선택이나 전처리 누락은 예측 성능 저하로 이어지고, 디버깅에 시간을 낭비하게 됩니다.

선형 회귀는 연속값 예측에서 가장 널리 쓰이는 알고리즘이지만, 제대로 사용하려면 .fit().predict()만 호출해서 끝나지 않습니다. 모델이 내부적으로 어떻게 동작하는지, 언제 실패하는지, 어떻게 평가해야 하는지, 그리고 언제 Ridge나 Lasso 같은 정규화 변형으로 전환해야 하는지 이해해야 합니다. 이런 단계를 건너뛰면 학습 데이터에서는 좋아 보이지만 새로운 데이터에서는 무너지는 모델을 배포하게 됩니다.

Scikit-learn은 LinearRegression을 제공할 뿐 아니라 전처리, 평가, 정규화를 위한 완전한 도구 생태계를 제공합니다. 이 가이드는 기본 사용법부터 프로덕션 수준의 회귀 파이프라인까지 전부 다룹니다.

📚

선형 회귀란?

선형 회귀는 하나 이상의 입력 특성과 연속형 출력 사이의 관계를, 잔차(residual)의 제곱합을 최소화하는 직선(또는 초평면)을 맞춰 모델링합니다. n개의 특성이 있는 모델의 식은 다음과 같습니다:

y = b0 + b1*x1 + b2*x2 + ... + bn*xn

여기서 b0는 절편(바이어스 항), b1...bn은 각 특성의 계수(가중치), y는 예측값입니다.

모델은 Ordinary Least Squares (OLS) 비용 함수를 최소화하는 계수를 찾습니다:

Cost = Sum of (y_actual - y_predicted)^2

이는 닫힌 형태(closed-form) 해가 존재하므로, 큰 데이터셋에서도 학습이 빠릅니다.

Sklearn으로 단순 선형 회귀

단순 선형 회귀는 단 하나의 특성으로 타깃을 예측합니다. 아래는 완전한 예시입니다:

from sklearn.linear_model import LinearRegression
import numpy as np
 
# Sample data: years of experience vs salary (in thousands)
X = np.array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]).reshape(-1, 1)
y = np.array([35, 40, 45, 55, 60, 62, 70, 75, 82, 90])
 
# Create and train the model
model = LinearRegression()
model.fit(X, y)
 
# Model parameters
print(f"Coefficient (slope): {model.coef_[0]:.4f}")
print(f"Intercept: {model.intercept_:.4f}")
 
# Predict salary for 12 years of experience
prediction = model.predict([[12]])
print(f"Predicted salary for 12 years: ${prediction[0]:.2f}k")
# Coefficient (slope): 5.9394
# Intercept: 28.3333
# Predicted salary for 12 years: $99.61k

출력 결과 이해하기

AttributeMeaningExample Value
model.coef_각 특성에 대한 가중치[5.94] -- 연봉이 1년당 약 $5,940 증가
model.intercept_모든 특성이 0일 때의 예측 y28.33 -- 기본 연봉 $28,330
model.score(X, y)주어진 데이터에서의 R-squared0.98

다중 선형 회귀

특성이 하나 이상이면 모델은 선 대신 초평면을 맞춥니다:

from sklearn.linear_model import LinearRegression
from sklearn.model_selection import train_test_split
from sklearn.datasets import fetch_california_housing
import numpy as np
 
# Load California housing dataset
housing = fetch_california_housing()
X, y = housing.data, housing.target
feature_names = housing.feature_names
 
print(f"Features: {feature_names}")
print(f"Dataset shape: {X.shape}")  # (20640, 8)
 
# Split into train and test
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42
)
 
# Train model
model = LinearRegression()
model.fit(X_train, y_train)
 
# Print coefficients for each feature
print("\nFeature Coefficients:")
for name, coef in zip(feature_names, model.coef_):
    print(f"  {name:12s}: {coef:+.6f}")
print(f"  {'Intercept':12s}: {model.intercept_:+.6f}")
 
# Evaluate
train_score = model.score(X_train, y_train)
test_score = model.score(X_test, y_test)
print(f"\nR² (train): {train_score:.4f}")
print(f"R² (test):  {test_score:.4f}")

모델 평가: R-squared, MSE, RMSE

R-squared만으로는 전체 상황을 충분히 설명하기 어렵습니다. 회귀 모델은 여러 지표로 평가하세요:

from sklearn.linear_model import LinearRegression
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score
from sklearn.datasets import fetch_california_housing
import numpy as np
 
housing = fetch_california_housing()
X_train, X_test, y_train, y_test = train_test_split(
    housing.data, housing.target, test_size=0.2, random_state=42
)
 
model = LinearRegression()
model.fit(X_train, y_train)
y_pred = model.predict(X_test)
 
# Calculate metrics
r2 = r2_score(y_test, y_pred)
mse = mean_squared_error(y_test, y_pred)
rmse = np.sqrt(mse)
mae = mean_absolute_error(y_test, y_pred)
 
print(f"R² Score:  {r2:.4f}")
print(f"MSE:       {mse:.4f}")
print(f"RMSE:      {rmse:.4f}")
print(f"MAE:       {mae:.4f}")
# R² Score:  0.5758
# MSE:       0.5559
# RMSE:      0.7456
# MAE:       0.5332

평가 지표 설명

MetricFormulaRangeInterpretation
R-squared (R²)1 - (SS_res / SS_tot)(-inf, 1]설명된 분산의 비율. 1.0=완벽, 0=평균 예측과 동일, 음수=평균보다도 나쁨
MSEmean((y - y_pred)²)[0, inf)평균 제곱 오차. 큰 오차에 더 큰 페널티
RMSEsqrt(MSE)[0, inf)타깃과 동일한 단위. MSE보다 해석이 쉬움
MAEmean(|y - y_pred|)[0, inf)평균 절대 오차. 이상치에 상대적으로 강건

낮은 R-squared가 항상 나쁜 모델을 의미하지는 않습니다. (주택 가격처럼) 노이즈가 많은 현실 데이터에서는 R² = 0.6도 충분히 합리적일 수 있습니다. 항상 RMSE를 타깃 변수의 스케일과 비교해 해석하세요.

선형 회귀에서의 특성 스케일링

기본 LinearRegression은 닫힌 형태 OLS 해를 사용하므로 특성 스케일링이 필수는 아닙니다. 하지만 정규화를 사용할 때는 스케일링이 매우 중요해집니다:

from sklearn.linear_model import LinearRegression, Ridge
from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import train_test_split
from sklearn.pipeline import Pipeline
from sklearn.datasets import fetch_california_housing
 
housing = fetch_california_housing()
X_train, X_test, y_train, y_test = train_test_split(
    housing.data, housing.target, test_size=0.2, random_state=42
)
 
# Without scaling (fine for basic LinearRegression)
model_no_scale = LinearRegression()
model_no_scale.fit(X_train, y_train)
print(f"LinearRegression R² (no scaling): {model_no_scale.score(X_test, y_test):.4f}")
 
# With scaling via Pipeline (required for regularized models)
pipeline = Pipeline([
    ('scaler', StandardScaler()),
    ('ridge', Ridge(alpha=1.0))
])
pipeline.fit(X_train, y_train)
print(f"Ridge R² (with scaling):          {pipeline.score(X_test, y_test):.4f}")

정규화에서 스케일링이 중요한 이유: Ridge와 Lasso는 큰 계수에 페널티를 동일한 기준으로 부여합니다. 어떤 특성은 01 범위이고 다른 특성은 0100,000 범위라면, 페널티가 불공정하게 적용되어 작은 범위의 특성 계수가 상대적으로 더 축소될 수 있습니다. 스케일링은 모든 특성을 같은 기준으로 맞춰 페널티가 공평하게 적용되도록 합니다.

다항 특성(Polynomial Features): 비선형 관계 모델링

특성과 타깃의 관계가 선형이 아닐 때는, 다항 특성으로 곡선과 상호작용을 포착할 수 있습니다:

from sklearn.linear_model import LinearRegression
from sklearn.preprocessing import PolynomialFeatures
from sklearn.pipeline import Pipeline
from sklearn.model_selection import train_test_split
from sklearn.metrics import r2_score
import numpy as np
 
# Generate non-linear data
np.random.seed(42)
X = np.linspace(0, 10, 200).reshape(-1, 1)
y = 3 * X.ravel()**2 - 5 * X.ravel() + 10 + np.random.randn(200) * 15
 
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42
)
 
# Linear model
linear = LinearRegression()
linear.fit(X_train, y_train)
print(f"Linear R²: {r2_score(y_test, linear.predict(X_test)):.4f}")
 
# Polynomial (degree 2) model
poly_pipeline = Pipeline([
    ('poly', PolynomialFeatures(degree=2, include_bias=False)),
    ('linear', LinearRegression())
])
poly_pipeline.fit(X_train, y_train)
print(f"Poly (d=2) R²: {r2_score(y_test, poly_pipeline.predict(X_test)):.4f}")
 
# Polynomial (degree 3) model
poly3_pipeline = Pipeline([
    ('poly', PolynomialFeatures(degree=3, include_bias=False)),
    ('linear', LinearRegression())
])
poly3_pipeline.fit(X_train, y_train)
print(f"Poly (d=3) R²: {r2_score(y_test, poly3_pipeline.predict(X_test)):.4f}")

주의: 고차 다항식은 매우 빠르게 과적합합니다. cross-validation으로 적절한 차수를 선택하고, 다항 모델에는 정규화를 우선 고려하세요.

정규화: Ridge, Lasso, ElasticNet

특성이 많거나 다항 항이 많은 경우, 정규화는 큰 계수에 페널티를 추가해 과적합을 방지합니다.

Ridge Regression (L2 Penalty)

Ridge는 비용 함수에 계수 제곱합을 더합니다. 계수를 0에 가깝게 줄이지만 정확히 0으로 만들지는 않습니다.

from sklearn.linear_model import Ridge
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline
from sklearn.model_selection import train_test_split, GridSearchCV
from sklearn.datasets import fetch_california_housing
import numpy as np
 
housing = fetch_california_housing()
X_train, X_test, y_train, y_test = train_test_split(
    housing.data, housing.target, test_size=0.2, random_state=42
)
 
# Find best alpha with cross-validation
pipeline = Pipeline([
    ('scaler', StandardScaler()),
    ('ridge', Ridge())
])
 
param_grid = {'ridge__alpha': [0.01, 0.1, 1.0, 10.0, 100.0]}
grid = GridSearchCV(pipeline, param_grid, cv=5, scoring='r2')
grid.fit(X_train, y_train)
 
print(f"Best alpha: {grid.best_params_['ridge__alpha']}")
print(f"Best CV R²: {grid.best_score_:.4f}")
print(f"Test R²:    {grid.score(X_test, y_test):.4f}")

Lasso Regression (L1 Penalty)

Lasso는 계수의 절댓값 합을 더합니다. 일부 계수를 정확히 0으로 만들 수 있어 자동 특성 선택(feature selection)을 수행합니다:

from sklearn.linear_model import Lasso
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline
from sklearn.model_selection import train_test_split, GridSearchCV
from sklearn.datasets import fetch_california_housing
import numpy as np
 
housing = fetch_california_housing()
X_train, X_test, y_train, y_test = train_test_split(
    housing.data, housing.target, test_size=0.2, random_state=42
)
 
pipeline = Pipeline([
    ('scaler', StandardScaler()),
    ('lasso', Lasso(max_iter=10000))
])
 
param_grid = {'lasso__alpha': [0.001, 0.01, 0.1, 1.0, 10.0]}
grid = GridSearchCV(pipeline, param_grid, cv=5, scoring='r2')
grid.fit(X_train, y_train)
 
print(f"Best alpha: {grid.best_params_['lasso__alpha']}")
print(f"Test R²:    {grid.score(X_test, y_test):.4f}")
 
# Show which features were selected (non-zero coefficients)
lasso_model = grid.best_estimator_.named_steps['lasso']
feature_names = housing.feature_names
for name, coef in zip(feature_names, lasso_model.coef_):
    status = "KEPT" if abs(coef) > 1e-6 else "DROPPED"
    print(f"  {name:12s}: {coef:+.6f}  [{status}]")

ElasticNet (L1 + L2 Penalty)

ElasticNet은 Ridge와 Lasso 페널티를 결합합니다. l1_ratio가 혼합 비율을 제어합니다: 0=순수 Ridge, 1=순수 Lasso.

from sklearn.linear_model import ElasticNet
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline
from sklearn.model_selection import train_test_split, GridSearchCV
from sklearn.datasets import fetch_california_housing
 
housing = fetch_california_housing()
X_train, X_test, y_train, y_test = train_test_split(
    housing.data, housing.target, test_size=0.2, random_state=42
)
 
pipeline = Pipeline([
    ('scaler', StandardScaler()),
    ('elasticnet', ElasticNet(max_iter=10000))
])
 
param_grid = {
    'elasticnet__alpha': [0.01, 0.1, 1.0],
    'elasticnet__l1_ratio': [0.1, 0.3, 0.5, 0.7, 0.9]
}
 
grid = GridSearchCV(pipeline, param_grid, cv=5, scoring='r2')
grid.fit(X_train, y_train)
 
print(f"Best alpha:    {grid.best_params_['elasticnet__alpha']}")
print(f"Best l1_ratio: {grid.best_params_['elasticnet__l1_ratio']}")
print(f"Test R²:       {grid.score(X_test, y_test):.4f}")

비교: LinearRegression vs Ridge vs Lasso vs ElasticNet

ModelPenaltyFeature SelectionWhen to UseScaling Required
LinearRegression없음아니오특성이 적고, 다중공선성이 없고, 신호 대비 노이즈가 좋은 경우아니오
RidgeL2 (제곱)아니오(0으로 수축)상관된 특성이 많고, 모든 특성을 유지하고 싶은 경우
LassoL1 (절댓값)예(계수를 0으로)특성이 많고 자동 특성 선택을 원할 때
ElasticNetL1 + L2예(부분적)상관된 특성이 많고 어느 정도 선택도 원할 때

올바른 모델 고르기

LinearRegression을 기준선(baseline)으로 사용하세요. 과적합(학습 R-squared와 테스트 R-squared의 격차가 큼)이 보이면 먼저 Ridge를 시도합니다. 불필요한 특성이 많다고 의심되면 Lasso를, 특성이 상관되어 있으면서 선택도 원하면 ElasticNet을 시도하세요. 항상 cross-validation으로 비교하는 것이 좋습니다.

선형 회귀의 가정(Assumptions)

선형 회귀는 다음 가정이 성립할 때 신뢰할 만한 결과를 냅니다:

  1. 선형성(Linearity) -- 특성과 타깃의 관계가 선형(또는 변환으로 선형화 가능)이다.
  2. 독립성(Independence) -- 관측치가 서로 독립이다. 시계열에서 자기상관을 고려하지 않으면 위배된다.
  3. 등분산성(Homoscedasticity) -- 예측값 수준 전반에 걸쳐 잔차 분산이 일정하다.
  4. 잔차 정규성(Normality of residuals) -- 잔차가 정규분포를 따른다. 예측 정확도보다는 신뢰구간/가설검정에 더 중요하다.
  5. 다중공선성 없음(No multicollinearity) -- 특성들끼리 강하게 상관되어 있지 않다. 다중공선성은 계수 분산을 키워 개별 계수 해석을 불안정하게 만든다.

코드로 가정 점검하기

from sklearn.linear_model import LinearRegression
from sklearn.model_selection import train_test_split
from sklearn.datasets import fetch_california_housing
import numpy as np
 
housing = fetch_california_housing()
X_train, X_test, y_train, y_test = train_test_split(
    housing.data, housing.target, test_size=0.2, random_state=42
)
 
model = LinearRegression()
model.fit(X_train, y_train)
y_pred = model.predict(X_test)
residuals = y_test - y_pred
 
# Check residual statistics
print(f"Residual mean:     {residuals.mean():.6f}")   # Should be near 0
print(f"Residual std:      {residuals.std():.4f}")
print(f"Residual skewness: {float(np.mean((residuals - residuals.mean())**3) / residuals.std()**3):.4f}")
 
# Check for multicollinearity (correlation matrix)
corr_matrix = np.corrcoef(X_train, rowvar=False)
print(f"\nMax feature correlation: {np.max(np.abs(corr_matrix - np.eye(corr_matrix.shape[0]))):.4f}")

완전한 파이프라인: 실전 회귀(Real-World Regression)

아래는 전처리, 특성 공학, 모델 선택을 결합한 프로덕션 스타일 파이프라인 예시입니다:

from sklearn.linear_model import LinearRegression, Ridge, Lasso
from sklearn.preprocessing import StandardScaler, PolynomialFeatures
from sklearn.model_selection import train_test_split, cross_val_score
from sklearn.pipeline import Pipeline
from sklearn.datasets import fetch_california_housing
import numpy as np
 
# Load data
housing = fetch_california_housing()
X_train, X_test, y_train, y_test = train_test_split(
    housing.data, housing.target, test_size=0.2, random_state=42
)
 
# Define models to compare
models = {
    'LinearRegression': Pipeline([
        ('scaler', StandardScaler()),
        ('model', LinearRegression())
    ]),
    'Ridge (alpha=1)': Pipeline([
        ('scaler', StandardScaler()),
        ('model', Ridge(alpha=1.0))
    ]),
    'Lasso (alpha=0.01)': Pipeline([
        ('scaler', StandardScaler()),
        ('model', Lasso(alpha=0.01, max_iter=10000))
    ]),
    'Poly(2) + Ridge': Pipeline([
        ('poly', PolynomialFeatures(degree=2, include_bias=False)),
        ('scaler', StandardScaler()),
        ('model', Ridge(alpha=10.0))
    ])
}
 
# Evaluate all models
print(f"{'Model':<25} {'CV R² (mean)':>12} {'CV R² (std)':>12} {'Test R²':>10}")
print("-" * 62)
 
for name, pipeline in models.items():
    cv_scores = cross_val_score(pipeline, X_train, y_train, cv=5, scoring='r2')
    pipeline.fit(X_train, y_train)
    test_r2 = pipeline.score(X_test, y_test)
    print(f"{name:<25} {cv_scores.mean():>12.4f} {cv_scores.std():>12.4f} {test_r2:>10.4f}")

PyGWalker로 회귀 결과 탐색하기

모델을 학습한 뒤에는 예측 패턴을 이해하는 것이 중요합니다. PyGWalker (opens in a new tab)를 사용하면 Jupyter에서 드래그 앤 드롭 인터페이스로 잔차(residual), feature importances, 실제값-예측값 관계를 인터랙티브하게 탐색할 수 있습니다:

import pandas as pd
import pygwalker as pyg
 
# Build a results DataFrame
results = pd.DataFrame(housing.data[len(X_train):], columns=housing.feature_names)
results['actual'] = y_test
results['predicted'] = y_pred
results['residual'] = y_test - y_pred
results['abs_error'] = np.abs(y_test - y_pred)
 
# Launch interactive exploration
walker = pyg.walk(results)

특성을 축으로 끌어다 놓고, 잔차 크기로 색상을 구분해 모델이 특히 어려워하는 데이터 구간을(플로팅 코드를 직접 작성하지 않고도) 확인할 수 있습니다.

Jupyter에서 반복적으로 실험을 수행할 때는, RunCell (opens in a new tab)이 제공하는 AI agent를 통해 서로 다른 특성 조합, 하이퍼파라미터, 전처리 단계를 셀을 매번 다시 쓰지 않고도 테스트할 수 있습니다.

FAQ

sklearn에서 LinearRegression이란?

sklearn.linear_model.LinearRegression은 Ordinary Least Squares (OLS) 회귀 모델입니다. 실제값과 예측값의 차이를 제곱해 합한 값을 최소화함으로써 데이터에 선형 방정식을 맞춥니다. scikit-learn에서 가장 단순하면서도 해석 가능한 회귀 모델입니다.

R-squared 점수는 어떻게 해석하나요?

R-squared는 타깃 변수의 분산 중 모델이 설명하는 비율을 의미합니다. R-squared가 0.80이면 분산의 80%를 설명한다는 뜻입니다. 1.0은 완벽한 적합, 0.0은 평균을 예측하는 것보다 나을 것이 없음을 의미하며, 음수 값은 평균 예측보다도 더 나쁘다는 뜻입니다.

Ridge vs Lasso vs ElasticNet은 언제 사용해야 하나요?

Ridge는 모든 특성을 유지하면서 과적합(특히 다중공선성이 있는 경우)을 줄이고 싶을 때 사용합니다. Lasso는 자동 특성 선택을 원할 때 사용하며(불필요한 특성의 계수를 0으로 만듦), ElasticNet은 특성들이 상관되어 있을 때 Ridge의 안정성과 Lasso의 희소성 사이에서 균형을 원할 때 사용합니다.

LinearRegression에 특성 스케일링이 필요한가요?

기본 LinearRegression은 OLS 해가 스케일에 대해 불변(scale-invariant)이므로 스케일링이 필수는 아닙니다. 하지만 Ridge, Lasso, ElasticNet은 페널티가 모든 계수 크기를 동일 기준으로 다루기 때문에 스케일링이 필요합니다. 정규화 회귀 전에는 항상 특성 스케일링을 하세요.

선형 회귀에서 범주형 특성은 어떻게 처리하나요?

OneHotEncoderpd.get_dummies()로 범주형 특성을 수치형으로 변환한 뒤 학습합니다. Sklearn의 LinearRegression은 수치 입력만 받습니다. 파이프라인에서는 ColumnTransformer를 사용해 수치형/범주형 컬럼에 서로 다른 변환을 적용할 수 있습니다.

MSE와 RMSE의 차이는 무엇인가요?

MSE(Mean Squared Error)는 실제값과 예측값 차이를 제곱해 평균낸 값입니다. RMSE(Root Mean Squared Error)는 MSE의 제곱근이며, 타깃 변수와 동일한 단위를 갖기 때문에 해석이 더 쉽습니다. 예를 들어 집값을 달러로 예측한다면 RMSE가 50,000이라는 뜻은 평균 예측 오차가 약 $50,000 정도라는 의미입니다.

결론

Sklearn의 LinearRegression은 Python에서 어떤 회귀 문제를 풀든 출발점이 되는 모델입니다. 빠르고, 해석 가능하며, 기본 관계가 대략 선형일 때 효과적입니다. 노이즈, 다중공선성, 많은 특성이 있는 현실 데이터셋에서는 Ridge, Lasso, ElasticNet 정규화가 일반화 성능을 개선해줍니다. 여러 지표(R-squared, RMSE, MAE)로 평가하고, train-test split으로 과적합을 피하며, 잔차 패턴을 점검해 모델 가정이 성립하는지 확인하세요. StandardScalerPolynomialFeatures를 파이프라인으로 구성하면 워크플로우를 깔끔하고 재현 가능하게 유지할 수 있습니다.

📚