ANL2001 commited on
Commit
dece3cd
Β·
1 Parent(s): 83ddfc8

feat: add ML preprocessing pipeline

Browse files

ManualFeatureProcessor handles imputation and engineers 8 derived
features (TotalSF, TotalQualSF, Age, TimeSinceRemod, etc.).
sklearn Pipeline builders for OHE and ordinal encoding.

Files changed (1) hide show
  1. ml/preprocessing.py +172 -0
ml/preprocessing.py ADDED
@@ -0,0 +1,172 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+ import pandas as pd
3
+ from sklearn.pipeline import Pipeline
4
+ from sklearn.compose import ColumnTransformer
5
+ from sklearn.preprocessing import OneHotEncoder, OrdinalEncoder
6
+ from sklearn.impute import SimpleImputer
7
+
8
+ # ─── Feature Definition ───────────────────────────────────────────────────────
9
+
10
+ CORE_FEATURES = [
11
+ # Numerical
12
+ "GrLivArea", "TotalBsmtSF", "LotArea", "GarageArea", "PoolArea", "LotFrontage",
13
+ "2ndFlrSF", "LowQualFinSF", "BsmtUnfSF", "1stFlrSF",
14
+ "WoodDeckSF", "OpenPorchSF", "EnclosedPorch", "3SsnPorch", "ScreenPorch",
15
+ # Counts
16
+ "FullBath", "HalfBath", "BsmtFullBath", "BsmtHalfBath", "TotRmsAbvGrd", "Fireplaces",
17
+ # Temporal
18
+ "YearBuilt", "YrSold", "YearRemodAdd",
19
+ # Quality / Condition
20
+ "OverallQual", "OverallCond", "HeatingQC", "BsmtQual", "PoolQC",
21
+ "ExterQual", "KitchenQual", "Functional", "FireplaceQu", "BsmtCond", "ExterCond",
22
+ # OneHot Categorical
23
+ "Neighborhood", "MSZoning", "MSSubClass",
24
+ "LandSlope", "Alley", "LandContour", "BldgType",
25
+ "Condition1", "RoofStyle", "Foundation",
26
+ "SaleCondition", "Exterior1st", "Utilities", "Electrical",
27
+ "GarageQual", "GarageCond",
28
+ ]
29
+
30
+ OHE_CATEGORICAL_COLS = [
31
+ "Neighborhood", "MSZoning", "LandSlope", "Alley", "LandContour", "BldgType",
32
+ "Condition1", "RoofStyle", "Foundation", "SaleCondition", "Exterior1st",
33
+ "Utilities", "Electrical", "GarageQual", "GarageCond",
34
+ ]
35
+
36
+ QUALITY_ORDER = ["Po", "Fa", "TA", "Gd", "Ex"]
37
+ FUNCTIONAL_ORDER = ["Sal", "Sev", "Maj2", "Maj1", "Mod", "Min2", "Min1", "Typ"]
38
+ QUALITY_COLS = [
39
+ "FireplaceQu", "BsmtCond", "KitchenQual", "ExterQual",
40
+ "HeatingQC", "BsmtQual", "PoolQC", "ExterCond",
41
+ ]
42
+ FUNCTIONAL_COLS = ["Functional"]
43
+
44
+ FILL_ZERO_COLS = [
45
+ "PoolArea", "GrLivArea", "LotArea", "TotalBsmtSF", "BsmtUnfSF",
46
+ "FullBath", "HalfBath", "BsmtFullBath", "BsmtHalfBath",
47
+ "2ndFlrSF", "LowQualFinSF", "1stFlrSF", "3SsnPorch",
48
+ "EnclosedPorch", "ScreenPorch", "WoodDeckSF", "OpenPorchSF", "GarageArea",
49
+ ]
50
+
51
+ SKEWED_FEATURES = [
52
+ "LotArea", "PoolArea", "LowQualFinSF", "BsmtHalfBath", "GrLivArea",
53
+ "LotFrontage", "1stFlrSF", "2ndFlrSF", "BsmtUnfSF",
54
+ "TotalSF", "TotalQualSF", "InteriorQualityScore",
55
+ ]
56
+
57
+ # ─── Preprocessing Functions ──────────────────────────────────────────────────
58
+
59
+ def outlier_removal(df: pd.DataFrame) -> pd.DataFrame:
60
+ idx = df[(df["GrLivArea"] > 4000) & (df["SalePrice"] < 300000)].index
61
+ return df.drop(idx, axis=0)
62
+
63
+
64
+ def fill_missing(df: pd.DataFrame) -> pd.DataFrame:
65
+ cols = [c for c in FILL_ZERO_COLS if c in df.columns]
66
+ df[cols] = df[cols].fillna(0)
67
+ return df
68
+
69
+
70
+ def add_engineered_features(df: pd.DataFrame) -> pd.DataFrame:
71
+ df["TotalSF"] = df["GrLivArea"] + df["TotalBsmtSF"]
72
+ df["TotalQualSF"] = df["TotalSF"] * df["OverallQual"]
73
+ df["TimeSinceRemod"] = df["YrSold"] - df["YearRemodAdd"]
74
+ df["Age"] = df["YrSold"] - df["YearBuilt"]
75
+ df["InteriorQualityScore"] = df["GrLivArea"] * df["OverallQual"]
76
+ df["TotalBaths"] = (
77
+ df["FullBath"] + 0.5 * df["HalfBath"]
78
+ + df["BsmtFullBath"] + 0.5 * df["BsmtHalfBath"]
79
+ )
80
+ porch_cols = ["WoodDeckSF", "OpenPorchSF", "EnclosedPorch", "3SsnPorch", "ScreenPorch"]
81
+ df["HasPorchDeck"] = (df[porch_cols].sum(axis=1) > 0).astype(int)
82
+ df["TotalPorchDeckSF"] = df[porch_cols].sum(axis=1)
83
+ return df
84
+
85
+
86
+ def log_transform_features(df: pd.DataFrame) -> pd.DataFrame:
87
+ for col in SKEWED_FEATURES:
88
+ if col in df.columns:
89
+ df[col] = np.log1p(df[col])
90
+ return df
91
+
92
+
93
+ # ─── Manual Feature Processor ─────────────────────────────────────────────────
94
+
95
+ class ManualFeatureProcessor:
96
+ """Fits on training data to learn imputation stats, then transforms any split."""
97
+
98
+ def __init__(self):
99
+ self.imputation_values = {}
100
+
101
+ def fit(self, X: pd.DataFrame) -> None:
102
+ if "LotFrontage" in X.columns:
103
+ self.imputation_values["LotFrontage"] = X["LotFrontage"].median()
104
+ if "YearBuilt" in X.columns:
105
+ self.imputation_values["YearBuilt_median"] = X["YearBuilt"].median()
106
+ if "YrSold" in X.columns:
107
+ self.imputation_values["YrSold_mode"] = X["YrSold"].mode()[0]
108
+
109
+ def transform(self, X: pd.DataFrame) -> pd.DataFrame:
110
+ X = X.copy()
111
+ if "LotFrontage" in self.imputation_values:
112
+ X["LotFrontage"] = X["LotFrontage"].fillna(self.imputation_values["LotFrontage"])
113
+ if "YearBuilt_median" in self.imputation_values:
114
+ X["YearBuilt"] = X["YearBuilt"].fillna(self.imputation_values["YearBuilt_median"])
115
+ X["YearRemodAdd"] = X["YearRemodAdd"].fillna(X["YearBuilt"])
116
+ if "YrSold_mode" in self.imputation_values:
117
+ X["YrSold"] = X["YrSold"].fillna(self.imputation_values["YrSold_mode"])
118
+ X["Utilities"] = X["Utilities"].fillna("AllPub")
119
+ X = fill_missing(X)
120
+ X = add_engineered_features(X)
121
+ X = log_transform_features(X)
122
+ return X
123
+
124
+
125
+ # ─── sklearn Pipeline Builders ────────────────────────────────────────────────
126
+
127
+ def build_ohe_preprocessor() -> ColumnTransformer:
128
+ categorical_pipeline = Pipeline(steps=[
129
+ ("imputer", SimpleImputer(strategy="constant", fill_value="missing")),
130
+ ("onehot", OneHotEncoder(
131
+ handle_unknown="infrequent_if_exist",
132
+ min_frequency=0.03,
133
+ sparse_output=False,
134
+ drop="first",
135
+ )),
136
+ ])
137
+ return ColumnTransformer(
138
+ transformers=[("cat", categorical_pipeline, OHE_CATEGORICAL_COLS)],
139
+ remainder="passthrough",
140
+ verbose_feature_names_out=False,
141
+ ).set_output(transform="pandas")
142
+
143
+
144
+ def build_ordinal_transformer() -> ColumnTransformer:
145
+ return ColumnTransformer(
146
+ transformers=[
147
+ ("quality_enc", Pipeline([
148
+ ("imputer", SimpleImputer(strategy="constant", fill_value="None")),
149
+ ("ordinal", OrdinalEncoder(
150
+ categories=[["None"] + QUALITY_ORDER] * len(QUALITY_COLS),
151
+ handle_unknown="use_encoded_value",
152
+ unknown_value=-1,
153
+ )),
154
+ ]), QUALITY_COLS),
155
+ ("functional_enc", Pipeline([
156
+ ("imputer", SimpleImputer(strategy="constant", fill_value="None")),
157
+ ("ordinal", OrdinalEncoder(
158
+ categories=[["None"] + FUNCTIONAL_ORDER],
159
+ handle_unknown="use_encoded_value",
160
+ unknown_value=-1,
161
+ )),
162
+ ]), FUNCTIONAL_COLS),
163
+ ],
164
+ remainder="passthrough",
165
+ )
166
+
167
+
168
+ def build_feature_pipeline() -> Pipeline:
169
+ return Pipeline(steps=[
170
+ ("ohe_proc", build_ohe_preprocessor()),
171
+ ("ordinal_prep", build_ordinal_transformer()),
172
+ ])