What is R-Squared In Data Science?

What is R-Squared?

R-squared (R²), also called the coefficient of determination, measures the proportion of variance in the dependent variable that is explained by the independent variables in a regression model.

"R-squared tells you how well your model fits the data—how much of the story your predictors tell."

The Intuition

Total Variation in Y = Explained Variation + Unexplained Variation
100%          =      R² × 100%       +    (1 - R²) × 100%
R² = 0.85 means: 85% of the variation in Y is explained by your model
15% is due to other factors or random noise

Part 1: Mathematical Foundation

1.1 The Formula

R² = 1 - (SS_res / SS_tot)
Where:
- SS_res = Sum of Squared Residuals (unexplained variation)
- SS_tot = Total Sum of Squares (total variation)
SS_res = Σ(y_i - ŷ_i)²
SS_tot = Σ(y_i - ȳ)²
Therefore:
R² = 1 - [Σ(y_i - ŷ_i)² / Σ(y_i - ȳ)²]

1.2 Visualizing R-Squared

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.linear_model import LinearRegression
from sklearn.metrics import r2_score
# Create demonstration data
np.random.seed(42)
n = 100
# Perfect relationship (R² = 1)
x_perfect = np.linspace(0, 10, n)
y_perfect = 2 * x_perfect + 1
# Moderate relationship (R² ≈ 0.7)
x_moderate = np.linspace(0, 10, n)
y_moderate = 2 * x_moderate + 1 + np.random.normal(0, 2, n)
# Weak relationship (R² ≈ 0.3)
x_weak = np.linspace(0, 10, n)
y_weak = 2 * x_weak + 1 + np.random.normal(0, 5, n)
# No relationship (R² ≈ 0)
x_none = np.linspace(0, 10, n)
y_none = np.random.normal(10, 3, n)
# Calculate R² for each
def calculate_r2(x, y):
model = LinearRegression()
model.fit(x.reshape(-1, 1), y)
y_pred = model.predict(x.reshape(-1, 1))
return r2_score(y, y_pred)
r2_perfect = calculate_r2(x_perfect, y_perfect)
r2_moderate = calculate_r2(x_moderate, y_moderate)
r2_weak = calculate_r2(x_weak, y_weak)
r2_none = calculate_r2(x_none, y_none)
# Visualize
fig, axes = plt.subplots(2, 2, figsize=(12, 10))
datasets = [
(x_perfect, y_perfect, f'Perfect Relationship\nR² = {r2_perfect:.3f}'),
(x_moderate, y_moderate, f'Moderate Relationship\nR² = {r2_moderate:.3f}'),
(x_weak, y_weak, f'Weak Relationship\nR² = {r2_weak:.3f}'),
(x_none, y_none, f'No Relationship\nR² = {r2_none:.3f}')
]
for ax, (x, y, title) in zip(axes.flat, datasets):
# Fit model
model = LinearRegression()
model.fit(x.reshape(-1, 1), y)
y_pred = model.predict(x.reshape(-1, 1))
# Plot
ax.scatter(x, y, alpha=0.6, label='Data')
ax.plot(x, y_pred, 'r-', linewidth=2, label='Regression Line')
# Add total variation arrows (to mean)
mean_y = np.mean(y)
ax.axhline(mean_y, color='gray', linestyle='--', alpha=0.5, label=f'Mean: {mean_y:.2f}')
# Annotate
ax.set_xlabel('X')
ax.set_ylabel('Y')
ax.set_title(title)
ax.legend()
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()

1.3 The Components of R-Squared

def decompose_r2(x, y):
"""
Decompose R² into its components
"""
# Fit model
model = LinearRegression()
model.fit(x.reshape(-1, 1), y)
y_pred = model.predict(x.reshape(-1, 1))
# Calculate components
y_mean = np.mean(y)
# Total Sum of Squares (total variation)
ss_tot = np.sum((y - y_mean) ** 2)
# Residual Sum of Squares (unexplained variation)
ss_res = np.sum((y - y_pred) ** 2)
# Explained Sum of Squares (explained variation)
ss_exp = np.sum((y_pred - y_mean) ** 2)
# R²
r2 = 1 - (ss_res / ss_tot)
# Create decomposition DataFrame
decomposition = pd.DataFrame({
'Component': ['Total Variation (SS_tot)', 
'Explained Variation (SS_exp)',
'Unexplained Variation (SS_res)'],
'Sum of Squares': [ss_tot, ss_exp, ss_res],
'Percentage': [100, (ss_exp/ss_tot)*100, (ss_res/ss_tot)*100]
})
print("=== R-SQUARED DECOMPOSITION ===\n")
print(decomposition.to_string())
print(f"\nR² = {r2:.4f} = {ss_exp:.2f} / {ss_tot:.2f}")
print(f"Interpretation: {r2*100:.1f}% of variation explained")
return decomposition
# Example
x_sample = np.linspace(0, 10, 50)
y_sample = 2 * x_sample + 1 + np.random.normal(0, 3, 50)
decomp = decompose_r2(x_sample, y_sample)

Part 2: R-Squared in Context

2.1 Range and Interpretation

def interpret_r2(r2, context="general"):
"""
Provide context-specific interpretation of R²
"""
interpretations = {
"general": [
(0.9, 1.0, "Excellent fit - almost all variation explained"),
(0.7, 0.9, "Very good fit - strong explanatory power"),
(0.5, 0.7, "Moderate fit - reasonable explanatory power"),
(0.3, 0.5, "Weak fit - limited explanatory power"),
(0.0, 0.3, "Poor fit - little explanatory power")
],
"social_sciences": [
(0.3, 1.0, "Good fit (human behavior is hard to predict)"),
(0.1, 0.3, "Moderate fit"),
(0.0, 0.1, "Weak fit")
],
"physical_sciences": [
(0.95, 1.0, "Expected fit (controlled experiments)"),
(0.8, 0.95, "Acceptable fit"),
(0.0, 0.8, "Poor fit - missing important factors")
],
"finance": [
(0.3, 1.0, "Good fit (markets are noisy)"),
(0.1, 0.3, "Moderate fit"),
(0.0, 0.1, "Typical for noisy financial data")
]
}
context_interpretations = interpretations.get(context, interpretations["general"])
print(f"R² = {r2:.4f}")
print(f"\nInterpretation for {context.replace('_', ' ').title()}:")
for lower, upper, desc in context_interpretations:
if lower <= r2 <= upper:
print(f"  → {desc}")
break
# Additional nuance
if r2 < 0:
print("\n⚠ Warning: R² < 0 indicates model performs worse than using the mean")
elif r2 > 1:
print("\n⚠ Warning: R² > 1 is impossible - check your calculations")
# Examples
interpret_r2(0.92, "physical_sciences")
interpret_r2(0.35, "social_sciences")
interpret_r2(0.12, "finance")

2.2 R-Squared for Different Model Types

from sklearn.ensemble import RandomForestRegressor
from sklearn.svm import SVR
from sklearn.neighbors import KNeighborsRegressor
def compare_r2_across_models(X, y, test_size=0.3):
"""
Compare R² across different model types
"""
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=test_size, random_state=42
)
models = {
'Linear Regression': LinearRegression(),
'Random Forest': RandomForestRegressor(n_estimators=100, random_state=42),
'SVM': SVR(),
'KNN': KNeighborsRegressor(n_neighbors=5)
}
results = []
for name, model in models.items():
model.fit(X_train, y_train)
y_pred = model.predict(X_test)
r2 = r2_score(y_test, y_pred)
results.append({
'Model': name,
'R² (Test)': r2,
'Interpretation': 'Good' if r2 > 0.7 else 'Moderate' if r2 > 0.5 else 'Poor'
})
results_df = pd.DataFrame(results).sort_values('R² (Test)', ascending=False)
print("=== R² COMPARISON ACROSS MODELS ===\n")
print(results_df.to_string())
return results_df

Part 3: Adjusted R-Squared

3.1 Why Adjusted R-Squared?

Problem: Adding more predictors always increases R², even if they're useless.

Solution: Adjusted R² penalizes for unnecessary predictors.

3.2 The Formula

Adjusted R² = 1 - [(1 - R²) × (n - 1) / (n - k - 1)]
Where:
- n = number of observations
- k = number of predictors

3.3 Demonstration

def demonstrate_adjusted_r2(n_samples=100):
"""
Show how adjusted R² penalizes useless predictors
"""
np.random.seed(42)
# Generate true relationship
X_true = np.random.randn(n_samples, 2)
y = 3 * X_true[:, 0] + 2 * X_true[:, 1] + np.random.randn(n_samples) * 0.5
results = []
# Add useless predictors one by one
for n_useless in range(0, 20):
# Create features: 2 true + n_useless random noise
X_useless = np.random.randn(n_samples, n_useless)
X = np.hstack([X_true, X_useless])
# Fit model
model = LinearRegression()
model.fit(X, y)
y_pred = model.predict(X)
# Calculate R² and Adjusted R²
n = len(y)
k = X.shape[1]
r2 = r2_score(y, y_pred)
adj_r2 = 1 - (1 - r2) * (n - 1) / (n - k - 1)
results.append({
'Predictors': k,
'Useless': n_useless,
'R²': r2,
'Adjusted R²': adj_r2,
'Difference': r2 - adj_r2
})
results_df = pd.DataFrame(results)
# Plot
fig, ax = plt.subplots(figsize=(10, 6))
ax.plot(results_df['Predictors'], results_df['R²'], 'b-', 
linewidth=2, label='R² (increases with useless predictors)')
ax.plot(results_df['Predictors'], results_df['Adjusted R²'], 'r-', 
linewidth=2, label='Adjusted R² (penalizes useless predictors)')
ax.axvline(x=2, color='gray', linestyle='--', 
label='True predictors (k=2)')
ax.set_xlabel('Number of Predictors (including useless)')
ax.set_ylabel('R² / Adjusted R²')
ax.set_title('R² vs Adjusted R²: Adding Useless Predictors')
ax.legend()
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
print("\n=== EFFECT OF USELESS PREDICTORS ===\n")
print(results_df[['Predictors', 'Useless', 'R²', 'Adjusted R²']].tail(10).to_string())
return results_df
results_df = demonstrate_adjusted_r2()

Part 4: Limitations and Pitfalls

4.1 What R-Squared Doesn't Tell You

def r2_limitations_demo():
"""
Demonstrate the limitations of R²
"""
np.random.seed(42)
n = 100
# Create different scenarios
scenarios = []
# 1. Non-linear relationship (R² can be misleading)
x_nonlinear = np.linspace(-5, 5, n)
y_nonlinear = x_nonlinear**2 + np.random.normal(0, 2, n)
# 2. Outliers affecting R²
x_outliers = np.random.randn(n)
y_outliers = 2 * x_outliers + np.random.randn(n)
# Add outliers
x_outliers = np.append(x_outliers, [5, 6, 7])
y_outliers = np.append(y_outliers, [30, 35, 40])
# 3. Heteroscedasticity (variance changes with X)
x_hetero = np.linspace(0, 10, n)
y_hetero = 2 * x_hetero + np.random.normal(0, x_hetero, n)
# 4. Restricted range (low R² but strong relationship)
x_restricted = np.random.uniform(8, 10, n)
y_restricted = 2 * x_restricted + np.random.normal(0, 0.5, n)
scenarios = [
(x_nonlinear, y_nonlinear, "Non-linear Relationship", 
"R² may be low even with strong relationship"),
(x_outliers, y_outliers, "Outliers Present",
"R² heavily influenced by outliers"),
(x_hetero, y_hetero, "Heteroscedasticity",
"R² doesn't detect varying variance"),
(x_restricted, y_restricted, "Restricted Range",
"R² low despite strong relationship within range")
]
fig, axes = plt.subplots(2, 2, figsize=(12, 10))
for ax, (x, y, title, note) in zip(axes.flat, scenarios):
# Fit linear model
model = LinearRegression()
model.fit(x.reshape(-1, 1), y)
y_pred = model.predict(x.reshape(-1, 1))
r2 = r2_score(y, y_pred)
# Plot
ax.scatter(x, y, alpha=0.6, s=30)
ax.plot(x, y_pred, 'r-', linewidth=2)
ax.set_xlabel('X')
ax.set_ylabel('Y')
ax.set_title(f'{title}\nR² = {r2:.3f}')
ax.text(0.05, 0.95, note, transform=ax.transAxes, 
fontsize=9, verticalalignment='top',
bbox=dict(boxstyle='round', facecolor='wheat', alpha=0.5))
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
r2_limitations_demo()

4.2 Common Misconceptions

def r2_misconceptions():
"""
Address common misconceptions about R²
"""
misconceptions = [
{
"Misconception": "High R² means the model is good",
"Truth": "High R² can come from overfitting, omitted variable bias, or spurious correlations",
"Example": "A model predicting stock prices with R²=0.95 but fails out-of-sample"
},
{
"Misconception": "Low R² means the model is useless",
"Truth": "Low R² can still have statistically significant predictors",
"Example": "Medical studies often have R² < 0.3 but identify important risk factors"
},
{
"Misconception": "R² measures prediction accuracy",
"Truth": "R² measures fit, not prediction accuracy",
"Example": "A model can have high R² but poor predictions if overfitted"
},
{
"Misconception": "Adding more variables always improves the model",
"Truth": "Adding useless variables increases R² but reduces adjusted R²",
"Example": "Including random noise as predictors inflates R² artificially"
},
{
"Misconception": "R² can be compared across different datasets",
"Truth": "R² is dataset-specific and not comparable across different Y variables",
"Example": "R² from a model predicting sales vs predicting log(sales) aren't comparable"
}
]
print("=== COMMON R-SQUARED MISCONCEPTIONS ===\n")
for i, mis in enumerate(misconceptions, 1):
print(f"{i}. {mis['Misconception']}")
print(f"   Truth: {mis['Truth']}")
print(f"   Example: {mis['Example']}\n")
# Visual example
np.random.seed(42)
n = 50
# Create data with high R² but poor predictions
x_train = np.linspace(0, 10, n)
y_train = 2 * x_train + np.random.normal(0, 0.5, n)
# Overfit model (high degree polynomial)
from sklearn.preprocessing import PolynomialFeatures
from sklearn.pipeline import make_pipeline
x_test = np.linspace(0, 12, 100)  # Test beyond training range
fig, axes = plt.subplots(1, 2, figsize=(12, 4))
# Good model (simple linear)
model_good = LinearRegression()
model_good.fit(x_train.reshape(-1, 1), y_train)
y_pred_good = model_good.predict(x_test.reshape(-1, 1))
r2_good = r2_score(y_train, model_good.predict(x_train.reshape(-1, 1)))
# Overfit model (high polynomial)
model_overfit = make_pipeline(PolynomialFeatures(degree=15), LinearRegression())
model_overfit.fit(x_train.reshape(-1, 1), y_train)
y_pred_overfit = model_overfit.predict(x_test.reshape(-1, 1))
r2_overfit = r2_score(y_train, model_overfit.predict(x_train.reshape(-1, 1)))
axes[0].scatter(x_train, y_train, alpha=0.6, label='Training Data')
axes[0].plot(x_test, y_pred_good, 'g-', linewidth=2, 
label=f'Good Model (R²={r2_good:.3f})')
axes[0].set_xlim(0, 12)
axes[0].set_title('Good Model: Generalizes Well')
axes[0].legend()
axes[0].grid(True, alpha=0.3)
axes[1].scatter(x_train, y_train, alpha=0.6, label='Training Data')
axes[1].plot(x_test, y_pred_overfit, 'r-', linewidth=2,
label=f'Overfit Model (R²={r2_overfit:.3f})')
axes[1].set_xlim(0, 12)
axes[1].set_title('Overfit Model: High R² but Poor Predictions')
axes[1].legend()
axes[1].grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
r2_misconceptions()

Part 5: Advanced R-Squared Concepts

5.1 Out-of-Sample R-Squared

def out_of_sample_r2(X, y, test_size=0.3):
"""
Calculate R² on test data to assess generalization
"""
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=test_size, random_state=42
)
model = LinearRegression()
model.fit(X_train, y_train)
# In-sample R²
y_train_pred = model.predict(X_train)
r2_train = r2_score(y_train, y_train_pred)
# Out-of-sample R²
y_test_pred = model.predict(X_test)
r2_test = r2_score(y_test, y_test_pred)
# Calculate shrinkage
shrinkage = (r2_train - r2_test) / r2_train * 100 if r2_train > 0 else 0
print("=== IN-SAMPLE vs OUT-OF-SAMPLE R² ===\n")
print(f"In-sample R² (Training): {r2_train:.4f}")
print(f"Out-of-sample R² (Test): {r2_test:.4f}")
print(f"Shrinkage: {shrinkage:.1f}%")
if r2_test < 0:
print("\n⚠ Warning: Test R² < 0 means model performs worse than using the mean")
print("   This indicates severe overfitting or wrong model specification")
return r2_train, r2_test

5.2 Pseudo R-Squared for Non-Linear Models

def pseudo_r2_logistic(y_true, y_pred_proba):
"""
Calculate various pseudo R² measures for logistic regression
"""
from sklearn.metrics import log_loss
# Null model (only intercept)
p_null = np.mean(y_true)
null_log_likelihood = np.sum(y_true * np.log(p_null) + 
(1 - y_true) * np.log(1 - p_null))
# Full model log-likelihood
full_log_likelihood = -log_loss(y_true, y_pred_proba, normalize=False)
# McFadden's R²
mcfadden_r2 = 1 - (full_log_likelihood / null_log_likelihood)
# Cox & Snell R²
n = len(y_true)
cox_snell = 1 - np.exp((2/n) * (null_log_likelihood - full_log_likelihood))
# Nagelkerke (adjusted Cox & Snell)
nagelkerke = cox_snell / (1 - np.exp(2 * null_log_likelihood / n))
results = {
'McFadden R²': mcfadden_r2,
'Cox & Snell R²': cox_snell,
'Nagelkerke R²': nagelkerke
}
print("=== PSEUDO R-SQUARED FOR LOGISTIC REGRESSION ===\n")
for name, value in results.items():
print(f"{name}: {value:.4f}")
return results

5.3 Incremental R-Squared (Change in R²)

def incremental_r2(df, target, base_vars, new_vars):
"""
Calculate the incremental contribution of new variables
"""
# Model without new variables
formula_base = f"{target} ~ {' + '.join(base_vars)}"
model_base = smf.ols(formula_base, data=df).fit()
# Model with new variables
all_vars = base_vars + new_vars
formula_full = f"{target} ~ {' + '.join(all_vars)}"
model_full = smf.ols(formula_full, data=df).fit()
# R² values
r2_base = model_base.rsquared
r2_full = model_full.rsquared
r2_increment = r2_full - r2_base
# F-test for significance of increment
from statsmodels.stats.anova import anova_lm
anova_results = anova_lm(model_base, model_full)
print("=== INCREMENTAL R-SQUARED ANALYSIS ===\n")
print(f"Base Model R²: {r2_base:.4f}")
print(f"Full Model R²: {r2_full:.4f}")
print(f"Incremental R²: {r2_increment:.4f}")
print(f"Percentage improvement: {(r2_increment/r2_base)*100:.1f}%")
print("\nANOVA Test for Improvement:")
print(anova_results)
return r2_increment, anova_results

Part 6: Practical Applications

6.1 Feature Selection Using R-Squared

def r2_based_feature_selection(X, y, threshold=0.01):
"""
Forward selection based on R² improvement
"""
from sklearn.feature_selection import f_regression
n_features = X.shape[1]
selected_features = []
remaining_features = list(range(n_features))
current_r2 = 0
history = []
while remaining_features:
best_r2 = current_r2
best_feature = None
for feature in remaining_features:
# Test adding this feature
features_to_test = selected_features + [feature]
X_subset = X[:, features_to_test]
model = LinearRegression()
model.fit(X_subset, y)
y_pred = model.predict(X_subset)
r2 = r2_score(y, y_pred)
if r2 > best_r2 + threshold:
best_r2 = r2
best_feature = feature
if best_feature is not None:
selected_features.append(best_feature)
remaining_features.remove(best_feature)
history.append({
'Step': len(selected_features),
'Feature': best_feature,
'R²': best_r2,
'ΔR²': best_r2 - current_r2
})
current_r2 = best_r2
else:
break
# Create results DataFrame
results_df = pd.DataFrame(history)
print("=== FORWARD SELECTION BASED ON R² ===\n")
print(results_df.to_string())
print(f"\nSelected {len(selected_features)} features")
print(f"Final R²: {current_r2:.4f}")
return selected_features, results_df

6.2 Model Comparison Framework

def comprehensive_model_comparison(models_dict, X_train, X_test, y_train, y_test):
"""
Compare multiple models with various R² metrics
"""
results = []
for model_name, model in models_dict.items():
# Fit model
model.fit(X_train, y_train)
# Predictions
y_train_pred = model.predict(X_train)
y_test_pred = model.predict(X_test)
# Calculate metrics
r2_train = r2_score(y_train, y_train_pred)
r2_test = r2_score(y_test, y_test_pred)
# Adjusted R² (for linear models only)
if hasattr(model, 'coef_'):
n = len(y_train)
k = X_train.shape[1]
adj_r2 = 1 - (1 - r2_train) * (n - 1) / (n - k - 1)
else:
adj_r2 = np.nan
# Overfitting measure
overfit_penalty = (r2_train - r2_test) / r2_train if r2_train > 0 else 0
results.append({
'Model': model_name,
'Train R²': r2_train,
'Test R²': r2_test,
'Adj. R²': adj_r2,
'Overfit Penalty': overfit_penalty,
'Generalization Gap': r2_train - r2_test
})
results_df = pd.DataFrame(results).sort_values('Test R²', ascending=False)
print("=== COMPREHENSIVE MODEL COMPARISON ===\n")
print(results_df.round(4).to_string())
# Visual comparison
fig, ax = plt.subplots(figsize=(10, 6))
x = np.arange(len(results_df))
width = 0.35
ax.bar(x - width/2, results_df['Train R²'], width, label='Train R²', alpha=0.8)
ax.bar(x + width/2, results_df['Test R²'], width, label='Test R²', alpha=0.8)
ax.set_xlabel('Model')
ax.set_ylabel('R²')
ax.set_title('Model Comparison: Train vs Test R²')
ax.set_xticks(x)
ax.set_xticklabels(results_df['Model'], rotation=45, ha='right')
ax.legend()
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
return results_df

Part 7: Reporting R-Squared

7.1 Best Practices for Reporting

def report_r2(r2, adj_r2=None, n=None, k=None, context="general"):
"""
Generate a professional report of R² results
"""
print("=== REGRESSION MODEL FIT REPORT ===\n")
# Basic R²
print(f"R-squared: {r2:.4f}")
print(f"Interpretation: {r2*100:.1f}% of the variance in the dependent variable "
f"is explained by the model.\n")
# Adjusted R² if provided
if adj_r2 is not None:
print(f"Adjusted R-squared: {adj_r2:.4f}")
if n and k:
print(f"Penalized for {k} predictors using {n} observations")
print(f"Difference: R² - Adj R² = {r2 - adj_r2:.4f}\n")
# Context-specific interpretation
interpret_r2(r2, context)
# Recommendations
print("\n--- Recommendations ---")
if r2 > 0.9:
print("✓ Very strong model fit")
print("  → Consider potential overfitting")
print("  → Validate with out-of-sample testing")
elif r2 > 0.7:
print("✓ Strong model fit")
print("  → Good explanatory power")
elif r2 > 0.5:
print("✓ Moderate model fit")
print("  → Consider additional predictors if needed")
elif r2 > 0.3:
print("⚠ Weak model fit")
print("  → May need additional variables or non-linear terms")
else:
print("⚠ Poor model fit")
print("  → Model may not be appropriate for this data")
print("  → Consider different variables or model types")
# Confidence interval for R² (bootstrap)
print("\n--- Confidence Interval (Bootstrap) ---")
print("Note: R² is a point estimate. Consider reporting confidence intervals.")

7.2 Visual Reporting

def visualize_r2_report(r2, adj_r2=None, model_name="Current Model"):
"""
Create visual report of R²
"""
fig, axes = plt.subplots(1, 3, figsize=(15, 4))
# 1. R² gauge chart
ax1 = axes[0]
# Create a gauge-like bar
colors = ['#d73027', '#fc8d59', '#fee08b', '#d9ef8b', '#91cf60', '#1a9850']
# Determine color based on R²
if r2 < 0.3:
color = colors[0]
status = "Poor"
elif r2 < 0.5:
color = colors[1]
status = "Weak"
elif r2 < 0.7:
color = colors[2]
status = "Moderate"
elif r2 < 0.9:
color = colors[3]
status = "Good"
else:
color = colors[4]
status = "Excellent"
# Create horizontal bar
ax1.barh([0], [r2], color=color, height=0.3, alpha=0.8)
ax1.barh([0], [1], color='lightgray', height=0.3, alpha=0.5)
ax1.set_xlim(0, 1)
ax1.set_yticks([])
ax1.set_xlabel('R-squared')
ax1.set_title(f'{model_name}\nR² = {r2:.3f} ({status})')
# Add text annotation
ax1.text(r2, 0, f'{r2:.1%}', ha='center', va='center', fontweight='bold')
# 2. Explained vs Unexplained pie chart
ax2 = axes[1]
sizes = [r2 * 100, (1 - r2) * 100]
labels = [f'Explained\n{r2:.1%}', f'Unexplained\n{1-r2:.1%}']
colors_pie = ['#2c7bb6', '#d7191c']
ax2.pie(sizes, labels=labels, colors=colors_pie, autopct='%1.1f%%', startangle=90)
ax2.set_title('Variance Decomposition')
# 3. Adjusted R² comparison (if provided)
ax3 = axes[2]
if adj_r2 is not None:
x = [0, 1]
y = [r2, adj_r2]
colors_bars = ['#2c7bb6', '#abd9e9']
bars = ax3.bar(x, y, color=colors_bars, alpha=0.8)
ax3.set_xticks(x)
ax3.set_xticklabels(['R²', 'Adjusted R²'])
ax3.set_ylim(0, 1)
ax3.set_ylabel('Value')
ax3.set_title('R² vs Adjusted R²')
# Add value labels
for bar, val in zip(bars, y):
ax3.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.02,
f'{val:.3f}', ha='center', fontweight='bold')
else:
# Show a comparison with a null model
ax3.text(0.5, 0.5, 'Adjusted R² not available\n(for non-linear models)',
ha='center', va='center', transform=ax3.transAxes, fontsize=10)
ax3.set_title('Adjusted R²')
ax3.set_xlim(0, 1)
ax3.set_ylim(0, 1)
plt.tight_layout()
plt.show()
# Example usage
visualize_r2_report(0.823, adj_r2=0.815, model_name="Sales Prediction Model")

Summary: R-Squared Quick Reference

# Quick Reference Card
r2_quick_reference = {
"Definition": "Proportion of variance explained by the model",
"Formula": "R² = 1 - (SS_res / SS_tot)",
"Range": "0 to 1 (or negative if model is worse than mean)",
"Interpretation": {
"0.90-1.00": "Excellent - almost all variation explained",
"0.70-0.90": "Good - strong explanatory power",
"0.50-0.70": "Moderate - reasonable explanatory power",
"0.30-0.50": "Weak - limited explanatory power",
"0.00-0.30": "Poor - little explanatory power"
},
"Key Limitations": [
"Doesn't measure prediction accuracy",
"Increases with useless predictors",
"Can't be compared across different Y variables",
"Doesn't detect non-linearity",
"Sensitive to outliers"
],
"When to Use": [
"Comparing models on same dataset",
"Assessing explanatory power",
"Feature selection (with caution)",
"Model improvement tracking"
],
"When NOT to Use": [
"Comparing models on different datasets",
"Assessing prediction performance alone",
"When assumptions are violated",
"As the only model evaluation metric"
],
"Alternatives": [
"Adjusted R² (penalizes complexity)",
"Out-of-sample R² (assesses generalization)",
"RMSE/MAE (prediction accuracy)",
"AIC/BIC (model comparison)",
"Pseudo R² (non-linear models)"
]
}
# Print quick reference
print("=== R-SQUARED QUICK REFERENCE ===\n")
for category, content in r2_quick_reference.items():
print(f"{category}:")
if isinstance(content, dict):
for key, value in content.items():
print(f"  {key}: {value}")
elif isinstance(content, list):
for item in content:
print(f"  • {item}")
else:
print(f"  {content}")
print()

Key Takeaway: R-squared is a powerful but often misunderstood metric. It tells you how much of the variance in your outcome is explained by your predictors, but it doesn't tell you if your model is good, if your predictions are accurate, or if you've chosen the right variables. Use R-squared as part of a comprehensive evaluation strategy that includes:

  1. Adjusted R² for model comparison
  2. Out-of-sample validation for generalization
  3. Diagnostic plots for assumption checking
  4. Subject matter expertise for practical significance
  5. Multiple metrics (RMSE, MAE, AIC, etc.) for a complete picture

Remember: A high R² doesn't guarantee a good model, and a low R² doesn't mean your model is useless. The right interpretation depends on your field, your data, and your specific goals.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper