Data Processing
Data cleaning, wrangling, and feature engineering using Python (Pandas/NumPy) efficiently — vectorized operations, missing data strategies, and encoding techniques for financial datasets.
Key Takeaways
apply()anditerrows()are almost always the wrong choice — use vectorized Pandas/NumPy operations.- MCAR (Missing Completely At Random), MAR (Missing At Random), MNAR (Missing Not At Random) — the type determines the right imputation strategy.
- One-hot encoding for low-cardinality categoricals; target encoding for high-cardinality (but watch for leakage).
- Log-transform right-skewed financial data (income, transaction amounts) before feeding to linear models.
pd.merge()default is inner join — explicitly specifyhow=to avoid silently dropping records.- Always sort by date before computing lag/rolling features; never shuffle time-indexed data.
1. Vectorized Operations
Pandas and NumPy operations are implemented in C under the hood. Iterating row-by-row in Python is orders of magnitude slower.
import pandas as pd
import numpy as np
# BAD: apply() is essentially a Python for-loop
df['risk_flag'] = df.apply(lambda row: 1 if row['amount'] > 5000 and row['days_late'] > 30 else 0, axis=1)
# GOOD: vectorized boolean mask — 100x+ faster on large DataFrames
df['risk_flag'] = ((df['amount'] > 5000) & (df['days_late'] > 30)).astype(int)
# GOOD: np.where() for conditional assignment
df['risk_label'] = np.where(df['risk_flag'] == 1, 'High', 'Normal')
# GOOD: vectorized string operation (not apply + lambda)
df['state'] = df['address'].str.extract(r',\s*([A-Z]{2})\s*\d{5}')
Performance Comparison
| Method | Relative Speed (10M rows) | Use When |
|---|---|---|
| NumPy / Pandas vectorized | 1x (baseline) | Almost always — first choice |
apply(axis=0) (column-wise) |
~5x slower | Column-level transformations not in NumPy |
apply(axis=1) (row-wise) |
~100–500x slower | Last resort only |
iterrows() |
~1000x slower | Never — always replace with vectorized |
2. Missing Data & Imputation
Types of Missingness
| Type | Definition | Financial Services Example | Strategy |
|---|---|---|---|
| MCAR | Missingness is truly random — unrelated to any variable | System glitch randomly drops income values | Mean/median imputation is valid; complete-case analysis is unbiased |
| MAR | Missingness depends on other observed variables, not on the missing value itself | Income missing more often for younger applicants | Conditional imputation (group-based mean, predictive models) |
| MNAR | Missingness depends on the missing value itself | High-income customers refuse to disclose income | Model the missingness mechanism; simple imputation introduces bias |
import pandas as pd
from sklearn.impute import SimpleImputer, KNNImputer
# Simple imputation
df['income'].fillna(df['income'].median(), inplace=True)
# Group-based imputation (MAR: income missing more for younger applicants)
df['income'] = df.groupby('age_band')['income'].transform(
lambda x: x.fillna(x.median())
)
# KNN imputation (uses neighboring rows)
imputer = KNNImputer(n_neighbors=5)
df[['income', 'credit_limit']] = imputer.fit_transform(df[['income', 'credit_limit']])
# Add missingness indicator before imputing (preserves MNAR signal)
df['income_missing'] = df['income'].isna().astype(int)
df['income'].fillna(df['income'].median(), inplace=True)
Outlier Treatment
# Winsorization: clip to 1st and 99th percentile
lower = df['amount'].quantile(0.01)
upper = df['amount'].quantile(0.99)
df['amount_clipped'] = df['amount'].clip(lower, upper)
# Log transformation: stabilize right-skewed distributions
df['log_amount'] = np.log1p(df['amount']) # log(x + 1) handles zeros
# Z-score outlier detection
z_scores = (df['amount'] - df['amount'].mean()) / df['amount'].std()
df['is_outlier'] = (z_scores.abs() > 3).astype(int)
3. Shaping & Merging
import pandas as pd
# GroupBy with multiple aggregations
summary = df.groupby('customer_id').agg(
total_spend=('amount', 'sum'),
txn_count=('amount', 'count'),
avg_txn=('amount', 'mean'),
last_txn_date=('txn_date', 'max')
).reset_index()
# Merging — always specify how= explicitly
merged = pd.merge(
customers,
transactions,
on='customer_id',
how='left' # keep all customers, even those with no transactions
)
print(f"Before merge: {len(customers)} rows")
print(f"After merge: {len(merged)} rows") # should equal len(customers) for left join
# Pivot: wide format
pivot = df.pivot_table(
index='customer_id',
columns='month',
values='spend',
aggfunc='sum',
fill_value=0
)
# Melt: long format
long_df = pivot.reset_index().melt(
id_vars='customer_id',
var_name='month',
value_name='spend'
)
4. Feature Engineering
Categorical Encoding
| Method | When to Use | Risk |
|---|---|---|
| One-Hot Encoding | Low cardinality (< 20 categories) | Curse of dimensionality at high cardinality |
| Label Encoding | Ordinal variables (e.g., credit tier: Bronze/Silver/Gold) | Implies false numeric ordering for nominal vars |
| Target Encoding | High cardinality (zip codes, merchant IDs) | Data leakage if not applied within CV folds |
| Frequency Encoding | High cardinality, tree-based models | Different categories with same frequency are indistinguishable |
import pandas as pd
from sklearn.preprocessing import StandardScaler
# One-hot encoding
df = pd.get_dummies(df, columns=['product_type'], drop_first=True)
# Target encoding with cross-validation (prevent leakage)
from category_encoders import TargetEncoder
encoder = TargetEncoder(cols=['merchant_category'])
# IMPORTANT: fit only on training data, then transform train and test separately
encoder.fit(X_train['merchant_category'], y_train)
X_train['merchant_category_enc'] = encoder.transform(X_train['merchant_category'])
X_test['merchant_category_enc'] = encoder.transform(X_test['merchant_category'])
# Scaling — fit ONLY on train, transform both
scaler = StandardScaler()
X_train[['income', 'balance']] = scaler.fit_transform(X_train[['income', 'balance']])
X_test[['income', 'balance']] = scaler.transform(X_test[['income', 'balance']])
# Time-series lag features — sort first!
df = df.sort_values('txn_date')
df['spend_lag_1'] = df.groupby('customer_id')['spend'].shift(1)
df['spend_roll_7'] = df.groupby('customer_id')['spend'].transform(
lambda x: x.rolling(7, min_periods=1).mean()
)
Practice Questions
Q1: What are MCAR, MAR, and MNAR? Give a financial services example of each.
- MCAR (Missing Completely At Random): Missingness is truly random — unrelated to any variable in the dataset. Example: a database sensor glitch randomly drops income values for 2% of records. Complete-case analysis is unbiased; mean/median imputation is valid.
- MAR (Missing At Random): Missingness depends on other observed variables but not on the missing value itself. Example: income is missing more often for younger applicants, but conditional on age, it is random. Group-based or model-based imputation is appropriate.
- MNAR (Missing Not At Random): Missingness depends on the missing value itself. Example: high-income customers refuse to disclose income. Simple imputation introduces bias — the imputed mean will underestimate true income for those customers. You must model the missingness mechanism explicitly, or add a missingness indicator feature.
Q2: You have a transaction amount column with extreme right skew (values from $1 to $500,000). What transformations do you apply, and why?
Log transformation: Apply log(amount + 1) to stabilize variance and
reduce skew. The +1 handles zero values. This makes the distribution closer to Normal,
which benefits linear models and distance-based models (KNN, SVM) that assume feature distributions are
roughly symmetric.
df['log_amount'] = np.log1p(df['amount']) # log(amount + 1)
Winsorization (alternative): Clip values at the 1st and 99th percentile to reduce outlier impact while preserving more signal than a hard cap:
lower, upper = df['amount'].quantile([0.01, 0.99])
df['amount_clipped'] = df['amount'].clip(lower, upper)
For tree-based models (XGBoost, Random Forest) neither transformation is strictly necessary — trees are invariant to monotonic transformations — but log-transform still helps if the target variable is skewed.
Q3: What is the risk of applying SMOTE before splitting into train/test sets?
Data leakage. SMOTE (Synthetic Minority Over-sampling Technique) generates synthetic minority samples by interpolating between real minority-class neighbors. If applied before splitting:
- Synthetic samples are created based on the full dataset, including real test-set observations.
- The synthetic training samples may be near-interpolations of real test samples — the model has indirectly "seen" the test set during training.
- Evaluation metrics will be artificially inflated.
Correct order: (1) Split into train/test. (2) Apply SMOTE to the training set only. (3) Evaluate on the untouched test set. Same principle applies to all resampling — oversampling, undersampling, and imputation.
Q4: Your colleague uses df.apply(some_function, axis=1) on a 10M-row DataFrame. What do you say?
apply(axis=1) iterates row-by-row in pure Python — it is essentially a for-loop with
overhead. On 10M rows this is extremely slow (potentially minutes vs. seconds for vectorized
equivalents).
Ask: can this operation be rewritten as a vectorized expression? Most operations can:
# Instead of apply — use vectorized alternatives:
# Conditional: np.where()
df['flag'] = np.where(df['amount'] > 5000, 1, 0)
# Multiple conditions: np.select()
conditions = [df['score'] >= 700, df['score'] >= 600]
choices = ['Good', 'Fair']
df['rating'] = np.select(conditions, choices, default='Poor')
# String operations: .str methods
df['city'] = df['address'].str.split(',').str[0].str.strip()
# Math: native arithmetic
df['ratio'] = df['balance'] / df['credit_limit'].replace(0, np.nan)
Only use apply() as a true last resort when the operation cannot be expressed as
NumPy/Pandas vectorized operations — for example, calling a third-party API per row or applying a
complex stateful function.
Action Items
- Write a script that standardizes messy date strings (multiple formats) into a
unified datetime object using
pd.to_datetime(). - Practice handling a DataFrame with 20% random missing values using both median imputation and group-based mean imputation.
- Replace an
apply(axis=1)call in a notebook with a vectorized NumPy equivalent and benchmark the difference. - Use
pd.melt()to unpivot a wide dataset andpivot_table()to reshape it back. - Implement target encoding for a high-cardinality column correctly — fit encoder only on training data, transform both train and test separately.