Feature Engineering
Feature engineering creates new variables from existing data. Good features give models more signal, and on tabular data they often matter more than the choice of algorithm. This page uses a synthetic clinical dataset with numeric, date, text, and categorical columns.
Every code block runs in a container in the companion repository, so the numbers are real.
import numpy as npimport pandas as pd
np.random.seed(42)n = 500df = pd.DataFrame({ "admission_date": pd.date_range("2020-01-01", periods=n, freq="2D"), "discharge_date": pd.date_range("2020-01-03", periods=n, freq="2D") + pd.to_timedelta(np.random.randint(1, 14, n), unit="D"), "age": np.random.normal(55, 15, n).astype(int), "weight_kg": np.random.normal(75, 15, n).round(1), "height_cm": np.random.normal(170, 10, n).round(1), "systolic_bp": np.random.normal(130, 20, n).astype(int), "diastolic_bp": np.random.normal(80, 12, n).astype(int), "glucose_mg_dl": np.random.lognormal(4.6, 0.3, n).round(1), "diagnosis_text": np.random.choice([ "Type 2 diabetes with neuropathy", "Hypertension stage 2", "Chronic kidney disease stage 3", "Acute myocardial infarction", "Heart failure with reduced ejection fraction"], n), "medication": np.random.choice( ["metformin", "lisinopril", "amlodipine", "insulin", "aspirin"], n), "lab_result": np.random.choice(["normal", "low", "high", "critical"], n),})Numeric transformations
Section titled “Numeric transformations”The most valuable features come from domain knowledge. Combine columns into quantities with known clinical meaning.
df["bmi"] = df["weight_kg"] / (df["height_cm"] / 100) ** 2df["pulse_pressure"] = df["systolic_bp"] - df["diastolic_bp"]df["map_pressure"] = df["diastolic_bp"] + df["pulse_pressure"] / 3df[["weight_kg", "height_cm", "bmi"]].head() weight_kg height_cm bmi 74.9 176.8 23.961682 95.4 167.3 34.084466 72.8 162.5 27.569231 64.8 178.7 20.292049 102.2 181.3 31.092475BMI, pulse pressure, and mean arterial pressure carry more meaning than the raw inputs.
Many biological measurements are right-skewed. A log transform brings them closer to
normal, which helps linear models. Use np.log1p to handle zeros.
print(f"Glucose skewness before log: {df['glucose_mg_dl'].skew():.2f}")df["log_glucose"] = np.log1p(df["glucose_mg_dl"])print(f"Glucose skewness after log: {df['log_glucose'].skew():.2f}")Glucose skewness before log: 0.86Glucose skewness after log: 0.11Binning turns a continuous variable into categories, useful when the effect changes at known thresholds.
df["age_group"] = pd.cut(df["age"], bins=[0, 40, 55, 65, 100], labels=["young", "middle", "senior", "elderly"])df["age_group"].value_counts().sort_index()young 91middle 161senior 113elderly 134Date features
Section titled “Date features”Dates are not directly usable. Extract numeric components and durations.
df["admission_month"] = df["admission_date"].dt.monthdf["admission_dayofweek"] = df["admission_date"].dt.dayofweekdf["length_of_stay"] = (df["discharge_date"] - df["admission_date"]).dt.daysdf["is_weekend_admission"] = df["admission_dayofweek"].isin([5, 6]).astype(int)admission_date admission_month admission_dayofweek length_of_stay is_weekend_admission 2020-01-01 1 2 9 0 2020-01-03 1 4 6 0 2020-01-05 1 6 15 1 2020-01-07 1 1 13 0 2020-01-09 1 3 10 0Month captures seasonality, day of week captures weekend effects, and time differences capture duration.
Text features
Section titled “Text features”Free-text fields hold signal the structured columns miss. Start with keyword flags.
df["diagnosis_word_count"] = df["diagnosis_text"].str.split().str.len()df["has_diabetes"] = df["diagnosis_text"].str.contains("diabetes", case=False).astype(int)df["has_heart"] = df["diagnosis_text"].str.contains("heart|myocardial|cardiac", case=False).astype(int)df["has_kidney"] = df["diagnosis_text"].str.contains("kidney|renal", case=False).astype(int) diagnosis_word_count has_diabetes has_heart has_kidney 6 0 1 0 3 0 1 0 3 0 1 0 6 0 1 0 6 0 1 0For more general text, TF-IDF turns text into weighted numeric vectors.
from sklearn.feature_extraction.text import TfidfVectorizer
tfidf = TfidfVectorizer(max_features=10, stop_words="english")mat = tfidf.fit_transform(df["diagnosis_text"])pd.DataFrame(mat.toarray(), columns=[f"tfidf_{w}" for w in tfidf.get_feature_names_out()]).head(3).round(3) tfidf_acute tfidf_ejection tfidf_heart tfidf_infarction tfidf_myocardial tfidf_reduced 0.000 0.577 0.577 0.000 0.000 0.577 0.577 0.000 0.000 0.577 0.577 0.000 0.577 0.000 0.000 0.577 0.577 0.000Encoding categorical features
Section titled “Encoding categorical features”For ordered categories, map to integers that reflect the order:
df["lab_result_ordinal"] = df["lab_result"].map({"normal": 0, "low": 1, "high": 2, "critical": 3})For high-cardinality categoricals, frequency encoding replaces each category with its prevalence, avoiding a wide one-hot expansion:
med_freq = df["medication"].value_counts(normalize=True)df["medication_freq"] = df["medication"].map(med_freq)med_freq.round(3)metformin 0.228lisinopril 0.226aspirin 0.202amlodipine 0.184insulin 0.160Polynomial and interaction features
Section titled “Polynomial and interaction features”PolynomialFeatures builds interaction terms automatically.
from sklearn.preprocessing import PolynomialFeatures
poly = PolynomialFeatures(degree=2, interaction_only=True, include_bias=False)poly.fit_transform(df[["age", "bmi", "glucose_mg_dl"]].head())poly.get_feature_names_out()Input features: 3, output features: 6Names: ['age', 'bmi', 'glucose', 'age bmi', 'age glucose', 'bmi glucose']interaction_only=True skips squared terms. Beware the explosion: 20 inputs at degree 2
give 210 features.
Guidelines
Section titled “Guidelines”| Technique | Best for | Watch out for |
|---|---|---|
| Domain features (BMI, ratios) | All models | Needs domain knowledge |
| Log transform | Skewed numerics | Helps tree models little |
| Binning | Known threshold effects | Loses information |
| Date extraction | Time-based data | Consider cyclical encoding |
| Keyword flags | Short text, known terms | Misses spelling variants |
| TF-IDF | Longer text | High dimensionality |
| Frequency encoding | High-cardinality categoricals | Compute inside CV to avoid leakage |
| Polynomial features | Linear models | Feature explosion |
Next steps
Section titled “Next steps”With features engineered and selected, the next pages cover specific algorithms. Start with Regression, the fundamental models every data scientist should understand.