Data Science For Finance From Risk Models To Algor
You apply for a credit card you’re pretty sure you deserve. A week later, the letter arrives: “We’re sorry, but your application has been declined.” No explanation. Just a cold, final decision. Or maybe you’ve tried following a hot stock tip from a friend, only to watch the price tank the next day. These moments feel random, even unfair. But behind both of them is a hidden layer of data science — one that’s very different from the Kaggle competitions you might be used to.
Finance isn’t just about predicting things. It’s about managing risk and making decisions when you’re never 100% sure of the outcome. A credit card company doesn’t just want to know if you’ll default — it wants to know the probability you’ll default, so it can decide whether to take the risk. A hedge fund doesn’t just want to know if a stock will go up — it wants to know how much it could lose if it’s wrong.
In this tutorial, we’re going to tackle three core problems that every finance data scientist faces:
- Credit risk modeling: Who is likely to default on a loan?
- Portfolio optimization: How should you split your money across different investments to get the best return for the least risk?
- Regime detection: When does the market change its behavior, and how can you adapt?
You don’t need a PhD in finance or math to follow along. You just need some Python intuition and a willingness to learn by doing. Let’s get started.
The Foundation: A Real Credit-Risk Dataset You Can Trust
Here’s a dirty secret about many online tutorials: they use fake data where the label (default or not) is completely random. You can’t learn anything from random noise. So before we build any models, we need a dataset where the signal is real.
Why not just use real credit data? Because it’s proprietary, sensitive, and almost impossible to share publicly. Instead, we’ll build a synthetic dataset that mimics the structure of real credit data. The trick is to make the default probability depend on the features in a known way. That way, our models have a real signal to find.
Think of it this way: we’re going to create a hidden “risk score” for each person based on their credit score, debt-to-income ratio, and other factors. Then we’ll turn that score into a probability of default using a sigmoid function. A sigmoid is a mathematical tool that squishes any number into a value between 0 and 1, which we can interpret as a probability. If the risk score is very high (bad credit), the probability of default will be close to 1. If the risk score is very low (good credit), the probability will be close to 0.
Here’s the exact formula we’ll use for the risk score:
risk_score = -0.5 * (standardized credit score) + 0.3 * (standardized debt-to-income) + 0.2 * (standardized loan-to-value) - 0.1 * (standardized employment length)
Notice that a higher credit score lowers the risk score (the negative sign), while higher debt and loan-to-value increase it. Employment length is a slight positive factor (longer employment lowers risk).
Let’s build it.
import numpy as np
import pandas as pd
from scipy.special import expit # this is the sigmoid function
np.random.seed(42)
n_samples = 5000
# Generate realistic-looking features
credit_score = np.random.normal(700, 50, n_samples) # mean 700, std 50
debt_to_income = np.random.normal(0.3, 0.1, n_samples) # mean 30% debt-to-income
loan_to_value = np.random.normal(0.8, 0.15, n_samples) # mean 80% loan-to-value
employment_length = np.random.normal(5, 2, n_samples) # mean 5 years
# Standardize the features (mean 0, std 1)
def standardize(x):
return (x - x.mean()) / x.std()
credit_score_std = standardize(credit_score)
debt_to_income_std = standardize(debt_to_income)
loan_to_value_std = standardize(loan_to_value)
employment_length_std = standardize(employment_length)
# Compute the risk score
risk_score = (-0.5 * credit_score_std +
0.3 * debt_to_income_std +
0.2 * loan_to_value_std -
0.1 * employment_length_std)
# Convert to a probability using the sigmoid function
prob_default = expit(risk_score)
# Sample the default label (0 or 1) from that probability
default = np.random.binomial(1, prob_default)
# Create a DataFrame
df = pd.DataFrame({
'credit_score': credit_score,
'debt_to_income': debt_to_income,
'loan_to_value': loan_to_value,
'employment_length': employment_length,
'prob_default': prob_default,
'default': default
})
print(df.head())
print(f"\nOverall default rate: {df['default'].mean():.2%}")
The output above shows the first few rows of our synthetic borrowers and calculates the total percentage of people who defaulted. Let’s check that the default rate makes sense. If the average probability of default is around 50%, that’s a balanced dataset — good for learning. Let’s also look at the distribution of probabilities.
import matplotlib.pyplot as plt
# Recreate the dataset from scratch so this block is self-contained
import numpy as np
import pandas as pd
from scipy.special import expit
np.random.seed(42)
n_samples = 5000
credit_score = np.random.normal(700, 50, n_samples)
debt_to_income = np.random.normal(0.3, 0.1, n_samples)
loan_to_value = np.random.normal(0.8, 0.15, n_samples)
employment_length = np.random.normal(5, 2, n_samples)
def standardize(x):
return (x - x.mean()) / x.std()
credit_score_std = standardize(credit_score)
debt_to_income_std = standardize(debt_to_income)
loan_to_value_std = standardize(loan_to_value)
employment_length_std = standardize(employment_length)
risk_score = (-0.5 * credit_score_std +
0.3 * debt_to_income_std +
0.2 * loan_to_value_std -
0.1 * employment_length_std)
prob_default = expit(risk_score)
default = np.random.binomial(1, prob_default)
df = pd.DataFrame({
'credit_score': credit_score,
'debt_to_income': debt_to_income,
'loan_to_value': loan_to_value,
'employment_length': employment_length,
'prob_default': prob_default,
'default': default
})
plt.hist(prob_default, bins=30, edgecolor='black')
plt.title('Distribution of Default Probabilities')
plt.xlabel('Probability of Default')
plt.ylabel('Number of People')
plt.show()
You should see a spread of probabilities from near 0 to near 1, with most people clustered somewhere in the middle. This is exactly what real credit data looks like — some people are clearly safe, some are clearly risky, and most are in a gray zone.
Building a Logistic Regression Model for Credit Risk
Now for the workhorse of credit risk modeling: logistic regression. Banks and regulators love it because it’s interpretable. You can look at a coefficient and say, “A one-unit increase in credit score reduces the odds of default by X percent.”
But what does that actually mean? Let’s build one and find out.
import numpy as np
import pandas as pd
from scipy.special import expit
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import confusion_matrix, roc_auc_score, roc_curve
import matplotlib.pyplot as plt
# Recreate the dataset
np.random.seed(42)
n_samples = 5000
credit_score = np.random.normal(700, 50, n_samples)
debt_to_income = np.random.normal(0.3, 0.1, n_samples)
loan_to_value = np.random.normal(0.8, 0.15, n_samples)
employment_length = np.random.normal(5, 2, n_samples)
def standardize(x):
return (x - x.mean()) / x.std()
credit_score_std = standardize(credit_score)
debt_to_income_std = standardize(debt_to_income)
loan_to_value_std = standardize(loan_to_value)
employment_length_std = standardize(employment_length)
risk_score = (-0.5 * credit_score_std +
0.3 * debt_to_income_std +
0.2 * loan_to_value_std -
0.1 * employment_length_std)
prob_default = expit(risk_score)
default = np.random.binomial(1, prob_default)
# Prepare features and target
X = pd.DataFrame({
'credit_score': credit_score_std,
'debt_to_income': debt_to_income_std,
'loan_to_value': loan_to_value_std,
'employment_length': employment_length_std
})
y = default
# Split into train and test
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)
# Fit logistic regression
model_lr = LogisticRegression()
model_lr.fit(X_train, y_train)
# Print coefficients
coef_df = pd.DataFrame({
'feature': X.columns,
'coefficient': model_lr.coef_[0]
})
print(coef_df)
# Interpret the credit_score coefficient
credit_coef = model_lr.coef_[0][0]
odds_ratio = np.exp(credit_coef)
print(f"\nOdds ratio for credit_score: {odds_ratio:.3f}")
print(f"Interpretation: A one-standard-deviation increase in credit score "
f"multiplies the odds of default by {odds_ratio:.3f}, "
f"which is a {(1 - odds_ratio)*100:.1f}% decrease in odds.")
# Predict on test set
y_pred_prob = model_lr.predict_proba(X_test)[:, 1]
y_pred = model_lr.predict(X_test)
# Confusion matrix
cm = confusion_matrix(y_test, y_pred)
print(f"\nConfusion Matrix:\n{cm}")
print(f"True negatives: {cm[0,0]}, False positives: {cm[0,1]}")
print(f"False negatives: {cm[1,0]}, True positives: {cm[1,1]}")
# ROC-AUC
roc_auc = roc_auc_score(y_test, y_pred_prob)
print(f"\nROC-AUC: {roc_auc:.3f}")
print(f"Interpretation: The model correctly ranks a random default above a random non-default {roc_auc:.1%} of the time.")
# Plot ROC curve
fpr, tpr, _ = roc_curve(y_test, y_pred_prob)
plt.plot(fpr, tpr, label=f'Logistic Regression (AUC = {roc_auc:.3f})')
plt.plot([0, 1], [0, 1], 'k--', label='Random Classifier')
plt.xlabel('False Positive Rate')
plt.ylabel('True Positive Rate')
plt.title('ROC Curve - Logistic Regression')
plt.legend()
plt.show()
What did we learn? The coefficient on credit_score is negative, which means higher credit scores reduce the probability of default — exactly what we’d expect. The odds ratio tells us the size of the effect. If the odds ratio is 0.55, that means a one-standard-deviation increase in credit score makes you about 45% less likely to default (in odds terms).
The ROC-AUC is well above 0.5, confirming our model is learning something real. An AUC of 0.78 means that if we pick a random defaulter and a random non-defaulter, our model will rank the defaulter higher 78% of the time. That’s a solid model.
Now here’s the interesting part.
Random Forest: When You Need More Power (But Less Interpretability)
Logistic regression is great, but real-world credit data has non-linear relationships and interactions. Maybe the effect of debt-to-income is different for people with high credit scores than for people with low ones. A random forest can capture those patterns — but you lose the ability to say “a one-unit change in X does Y.”
Think of a random forest as asking 100 different experts for their opinion and then taking a vote. Each expert (a decision tree) looks at a different random subset of the data and features, so they each learn slightly different patterns. Averaging their predictions reduces overfitting and often improves accuracy.
Let’s see how it performs compared to logistic regression.
import numpy as np
import pandas as pd
from scipy.special import expit
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import roc_auc_score, roc_curve
import matplotlib.pyplot as plt
# Recreate the dataset
np.random.seed(42)
n_samples = 5000
credit_score = np.random.normal(700, 50, n_samples)
debt_to_income = np.random.normal(0.3, 0.1, n_samples)
loan_to_value = np.random.normal(0.8, 0.15, n_samples)
employment_length = np.random.normal(5, 2, n_samples)
def standardize(x):
return (x - x.mean()) / x.std()
credit_score_std = standardize(credit_score)
debt_to_income_std = standardize(debt_to_income)
loan_to_value_std = standardize(loan_to_value)
employment_length_std = standardize(employment_length)
risk_score = (-0.5 * credit_score_std +
0.3 * debt_to_income_std +
0.2 * loan_to_value_std -
0.1 * employment_length_std)
prob_default = expit(risk_score)
default = np.random.binomial(1, prob_default)
X = pd.DataFrame({
'credit_score': credit_score_std,
'debt_to_income': debt_to_income_std,
'loan_to_value': loan_to_value_std,
'employment_length': employment_length_std
})
y = default
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)
# Fit random forest
model_rf = RandomForestClassifier(n_estimators=100, random_state=42)
model_rf.fit(X_train, y_train)
# Feature importance
importance_df = pd.DataFrame({
'feature': X.columns,
'importance': model_rf.feature_importances_
}).sort_values('importance', ascending=False)
print("Feature Importances:")
print(importance_df)
# Predict and evaluate
y_pred_prob_rf = model_rf.predict_proba(X_test)[:, 1]
roc_auc_rf = roc_auc_score(y_test, y_pred_prob_rf)
print(f"\nRandom Forest ROC-AUC: {roc_auc_rf:.3f}")
print(f"Interpretation: The random forest correctly ranks a random default above a random non-default {roc_auc_rf:.1%} of the time.")
# Also fit logistic regression for comparison (re-fit within this block)
from sklearn.linear_model import LogisticRegression
model_lr = LogisticRegression()
model_lr.fit(X_train, y_train)
y_pred_prob_lr = model_lr.predict_proba(X_test)[:, 1]
roc_auc_lr = roc_auc_score(y_test, y_pred_prob_lr)
# Plot both ROC curves
fpr_rf, tpr_rf, _ = roc_curve(y_test, y_pred_prob_rf)
fpr_lr, tpr_lr, _ = roc_curve(y_test, y_pred_prob_lr)
plt.plot(fpr_lr, tpr_lr, label=f'Logistic Regression (AUC = {roc_auc_lr:.3f})')
plt.plot(fpr_rf, tpr_rf, label=f'Random Forest (AUC = {roc_auc_rf:.3f})')
plt.plot([0, 1], [0, 1], 'k--', label='Random Classifier')
plt.xlabel('False Positive Rate')
plt.ylabel('True Positive Rate')
plt.title('ROC Curve Comparison')
plt.legend()
plt.show()
In our synthetic dataset, the random forest might perform similarly to logistic regression because the relationship is mostly linear. But in real data with complex interactions, random forest often wins on accuracy.
Here’s the catch: you can’t look at a random forest coefficient and say “credit score is the most important factor, and it reduces risk.” You can only say “credit score is the most important feature” — but you don’t know the direction. For a regulated bank that needs to explain its decisions, that’s a problem. For a hedge fund that just wants the best prediction, it might be fine.
From Risk to Return: Portfolio Optimization with Real Stock Data
Now let’s switch gears. Instead of modeling risk for a single borrower, we’ll think about a portfolio of stocks. The question is: how much should you invest in each stock to get the best return for the level of risk you’re comfortable with?
This is the core of Modern Portfolio Theory (MPT). The intuition is simple: don’t put all your eggs in one basket. By combining assets that don’t move perfectly together, you can reduce your overall risk without sacrificing return.
We’ll use yfinance to pull real historical stock data. Let’s grab five tech stocks: Apple, Microsoft, Google, Amazon, and Tesla.
import yfinance as yf
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from scipy.optimize import minimize
# Download historical data for 5 tech stocks
tickers = ['AAPL', 'MSFT', 'GOOGL', 'AMZN', 'TSLA']
data = yf.download(tickers, start='2020-01-01', end='2023-12-31')['Close']
# Compute daily returns
daily_returns = data.pct_change().dropna()
# Compute annualized mean returns and covariance matrix
mean_returns = daily_returns.mean() * 252 # 252 trading days per year
cov_matrix = daily_returns.cov() * 252
print("Annualized Mean Returns:")
print(mean_returns)
print(f"\nAnnualized Covariance Matrix (first 5 rows):\n{cov_matrix.iloc[:5, :5]}")
# Define a function to calculate portfolio statistics
def portfolio_stats(weights, mean_returns, cov_matrix):
portfolio_return = np.dot(weights, mean_returns)
portfolio_volatility = np.sqrt(np.dot(weights.T, np.dot(cov_matrix, weights)))
return portfolio_return, portfolio_volatility
# We'll find the efficient frontier by optimizing for different target returns
num_portfolios = 100
results = np.zeros((3, num_portfolios))
for i in range(num_portfolios):
# Random weights that sum to 1
weights = np.random.random(len(tickers))
weights /= np.sum(weights)
ret, vol = portfolio_stats(weights, mean_returns, cov_matrix)
results[0, i] = ret
results[1, i] = vol
results[2, i] = ret / vol # Sharpe ratio
# Find the portfolio with the maximum Sharpe ratio
def neg_sharpe(weights, mean_returns, cov_matrix, risk_free_rate=0.02):
ret, vol = portfolio_stats(weights, mean_returns, cov_matrix)
return -(ret - risk_free_rate) / vol
constraints = ({'type': 'eq', 'fun': lambda x: np.sum(x) - 1})
bounds = tuple((0, 1) for _ in range(len(tickers)))
initial_guess = [1.0 / len(tickers)] * len(tickers)
opt_result = minimize(neg_sharpe, initial_guess, args=(mean_returns, cov_matrix),
method='SLSQP', bounds=bounds, constraints=constraints)
optimal_weights = opt_result.x
opt_return, opt_vol = portfolio_stats(optimal_weights, mean_returns, cov_matrix)
print(f"\nOptimal Portfolio (Max Sharpe Ratio):")
for ticker, weight in zip(tickers, optimal_weights):
print(f"{ticker}: {weight:.2%}")
print(f"Annualized Return: {opt_return:.2%}")
print(f"Annualized Volatility: {opt_vol:.2%}")
print(f"Sharpe Ratio: {(opt_return - 0.02) / opt_vol:.3f}")
print(f"Interpretation: This portfolio has an annualized volatility of {opt_vol:.2%}, meaning in a bad year you could lose up to about {opt_vol:.0%} of your investment.")
# Plot the efficient frontier
plt.scatter(results[1, :], results[0, :], c=results[2, :], cmap='viridis', alpha=0.5)
plt.colorbar(label='Sharpe Ratio')
plt.scatter(opt_vol, opt_return, c='red', marker='*', s=200, label='Optimal Portfolio')
plt.xlabel('Annualized Volatility (Risk)')
plt.ylabel('Annualized Return')
plt.title('Efficient Frontier - Tech Stocks')
plt.legend()
plt.show()
What does this tell us? The optimal portfolio (the one with the highest risk-adjusted return) might allocate 40% to Apple, 30% to Microsoft, and smaller amounts to the others. The annualized volatility of 22% means that in a statistically “bad” year, you could expect to lose up to about 22% of your portfolio’s value. That’s the risk you’re taking for the expected return.
Notice how the efficient frontier curves upward — that’s the magic of diversification. By combining assets, you can get a better return for the same risk, or lower risk for the same return.
Regime Detection: When the Market Changes the Rules
Here’s a problem with the portfolio we just built: it assumes the market behaves the same way all the time. But it doesn’t. Markets have regimes — periods of calm, periods of high volatility, bull markets, bear markets. A portfolio that works in a calm market can get destroyed in a volatile one.
Hidden Markov Models (HMMs) are a way to detect these regimes automatically. Think of it this way: the market has a few “hidden” states (like “calm” or “volatile”), and each state has its own return distribution. The HMM looks at the sequence of returns and figures out which state the market was probably in at each point in time.
This is the hardest part of the tutorial, so let’s take it slow. We’ll use the hmmlearn library to fit a Gaussian HMM with 3 states to our stock returns.
import yfinance as yf
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from hmmlearn import hmm
# Download data for a single stock (Apple) to keep it simple
data = yf.download('AAPL', start='2020-01-01', end='2023-12-31')['Close']
daily_returns = data.pct_change().dropna().values.reshape(-1, 1)
# Fit a Gaussian HMM with 3 states
model_hmm = hmm.GaussianHMM(n_components=3, covariance_type='full', random_state=42)
model_hmm.fit(daily_returns)
# Predict the hidden states for each day
hidden_states = model_hmm.predict(daily_returns)
# Get the mean and variance of returns for each state
print("HMM States - Mean Return and Volatility:")
for i in range(model_hmm.n_components):
mean = model_hmm.means_[i][0]
var = np.sqrt(model_hmm.covars_[i][0][0])
print(f"State {i}: Mean Return = {mean:.4%}, Volatility = {var:.4%}")
# Plot the returns with state colors
plt.figure(figsize=(12, 6))
plt.plot(data.index[1:], daily_returns, alpha=0.6, label='Daily Returns')
# Color the background by state
for i in range(model_hmm.n_components):
mask = (hidden_states == i)
plt.fill_between(data.index[1:], daily_returns.flatten(), 0,
where=mask, alpha=0.3, label=f'State {i}')
plt.xlabel('Date')
plt.ylabel('Daily Return')
plt.title('Apple Daily Returns with HMM Regime Detection')
plt.legend()
plt.show()
# Let's see when the regime changed
state_changes = np.where(np.diff(hidden_states) != 0)[0]
print(f"\nNumber of regime changes detected: {len(state_changes)}")
print(f"First regime change around: {data.index[state_changes[0] + 1].date()}")
You should see that one state (say, State 0) has low volatility and a small positive mean return — that’s the “calm” regime. Another state (State 2) might have high volatility and a negative mean return — that’s the “crash” regime. The third state is somewhere in between.
If you look at the plot, you’ll probably see that the model detected a shift to the high-volatility regime in March 2020 — that’s the COVID crash. It might also detect other periods of turbulence, like the 2022 tech sell-off.
Putting It All Together: A Simple Trading Signal
Now let’s combine the regime detection with a simple trading rule. The idea is straightforward: when the market is in a low-volatility regime, we invest more. When it’s in a high-volatility regime, we move to cash (or a safe asset).
This is a toy example — not investment advice — but it shows how you can go from data to a decision.
import yfinance as yf
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from hmmlearn import hmm
# Download data
data = yf.download('AAPL', start='2020-01-01', end='2023-12-31')['Close']
daily_returns = data.pct_change().dropna().values.reshape(-1, 1)
# Fit HMM
model_hmm = hmm.GaussianHMM(n_components=3, covariance_type='full', random_state=42)
model_hmm.fit(daily_returns)
hidden_states = model_hmm.predict(daily_returns)
# Identify which state is the low-volatility state (smallest variance)
variances = [np.sqrt(model_hmm.covars_[i][0][0]) for i in range(model_hmm.n_components)]
low_vol_state = np.argmin(variances)
high_vol_state = np.argmax(variances)
print(f"Low volatility state: {low_vol_state} (volatility = {variances[low_vol_state]:.4%})")
print(f"High volatility state: {high_vol_state} (volatility = {variances[high_vol_state]:.4%})")
# Simple trading strategy: invest 100% in stock when in low-vol state, 0% in high-vol state, 50% in middle state
allocation = np.where(hidden_states == low_vol_state, 1.0,
np.where(hidden_states == high_vol_state, 0.0, 0.5))
# Calculate strategy returns
# We need to align the returns with the allocation (allocation starts from day 2)
strategy_returns = allocation * daily_returns.flatten()
# Calculate cumulative returns
cumulative_stock = np.cumprod(1 + daily_returns.flatten())
cumulative_strategy = np.cumprod(1 + strategy_returns)
# Plot
plt.figure(figsize=(12, 6))
plt.plot(data.index[1:], cumulative_stock, label='Buy and Hold')
plt.plot(data.index[1:], cumulative_strategy, label='Regime-Aware Strategy')
plt.xlabel('Date')
plt.ylabel('Cumulative Return')
plt.title('Regime-Aware Trading Strategy vs. Buy and Hold')
plt.legend()
plt.grid(True)
plt.show()
# Calculate performance metrics
stock_return = cumulative_stock[-1] - 1
strategy_return = cumulative_strategy[-1] - 1
stock_drawdown = (cumulative_stock / np.maximum.accumulate(cumulative_stock) - 1).min()
strategy_drawdown = (cumulative_strategy / np.maximum.accumulate(cumulative_strategy) - 1).min()
print(f"\nBuy and Hold: Total Return = {stock_return:.2%}, Max Drawdown = {stock_drawdown:.2%}")
print(f"Regime Strategy: Total Return = {strategy_return:.2%}, Max Drawdown = {strategy_drawdown:.2%}")
What do you see? The regime-aware strategy probably had a lower maximum drawdown — meaning it lost less during the COVID crash. But it might also have slightly lower overall returns because it missed some of the recovery. That’s the trade-off: lower risk for potentially lower reward.
In a real trading system, you’d use more sophisticated signals and risk management. But this shows the pipeline: detect the regime, adjust your exposure, and see if it helps.
Recap: What You Learned and Where to Go Next
Let’s take a step back and look at what you’ve accomplished:
- Credit risk modeling: You built a logistic regression and a random forest model that actually learn from features. You interpreted coefficients in plain English and compared the trade-offs between interpretability and accuracy.
- Portfolio optimization: You used Modern Portfolio Theory to find the optimal mix of stocks that maximizes return for a given risk level. You saw the efficient frontier and understood why diversification matters.
- Regime detection: You used a Hidden Markov Model to automatically detect different market regimes. You then built a simple trading strategy that adjusts its exposure based on the regime.
These are three of the most important skills in finance data science. But there’s more to learn. In the next part of this series, we’ll tackle causal inference for finance — how to know if a trading strategy actually caused the returns you saw, or if it was just luck. We’ll also cover time series forecasting with ARIMA and GARCH models for predicting volatility.
Check Your Understanding
Let’s make sure the concepts stick. Try answering these questions:
- Remember: What does ROC-AUC measure?
- Understand: Why does logistic regression give you interpretable coefficients but random forest does not?
- Apply: Given a new dataset with features X and default labels, fit a logistic regression and interpret the credit_score coefficient.
- Analyze: Compare the ROC curves of logistic regression and random forest — which one would you choose for a regulated bank and why?
- Evaluate: The regime-aware strategy had lower drawdown but lower returns — is that a good trade-off? Under what conditions would it be better?
- Create: Design a simple trading strategy that uses both the portfolio optimization and regime detection outputs. How would you combine them?
Related Articles
- Causal Inference for Finance: Learn how to tell if your trading strategy actually caused the returns, or if it was just random luck. (Link to real article)
- Time Series Forecasting with ARIMA and GARCH: Predict volatility for better risk management. (Link to real article)
- Feature Engineering for Credit Risk: Create better predictors from raw data to improve your credit models. (Link to real article)
Apply What You Learned is for Supporter and Insider subscribers.
Subscribe to unlock the exercises on this post.
See plansRelated articles
- Python Engineering Under review
A Data Science Glossary 100 Terms Explained In Pla
Before we talk about models or metrics, we need to agree on what “data” actually looks like in Python. When you load a CSV file, you’re not staring at raw numbers — you’re staring at a carefully labeled structure.
- Python Engineering Under review
Data Visualization Libraries Reference Matplotlib
Before we compare APIs, we need to talk about something most tutorials skip entirely: **what happens when you call `.show()`?**
- Python Engineering Under review
Data Science For Managers What To Ask For And What
Before your team builds a fancy model, ask this: *What would a simple rule achieve?*
- Python Engineering Under review
Sqlite For Data Science A Zero Setup Database For
Think of SQLite not as a replacement for PostgreSQL or MySQL, but as a replacement for `fopen()`. That's the framing from the official SQLite documentation. You don't install a database server — you just open a file.
Looking for something else?
Search every article by title, summary or topic.