The dataset for this competition (both train and test) was generated from a deep learning model trained on the California Housing Dataset. Feature distributions are close to, but not exactly the same, as the original. Feel free to use the original dataset as part of this competition, both to explore differences as well as to see whether incorporating the original in training improves model performance.
Files
Data is synthetic
Features:
House Characteristics
Spatial Features
Target Feature
We're also told that the dataset was "derived from the 1990 U.S. census"" and that they define a block to be "the smallest geographical unit for which the U.S. Census Bureau publishes sample data (a block group typically has a population of 600 to 3,000 people).".
Submissions are scored on the root mean squared error.
Import Libraries
¶import pandas as pd
import matplotlib.pyplot as plt
import tensorflow as tf
from tensorflow.keras import layers
import seaborn as sns
from sklearn.datasets import fetch_california_housing
Exploratory Data Analysis
¶data_train = pd.read_csv('/kaggle/input/playground-series-s3e1/train.csv', index_col='id')
data_test = pd.read_csv('/kaggle/input/playground-series-s3e1/test.csv', index_col='id')
features = data_train.columns[:-1]
original_df, original_y = fetch_california_housing(return_X_y=True)
original_df = pd.DataFrame(original_df, columns=features)
original_df['MedHouseVal'] = original_y
data_train.shape[0], data_test.shape[0], original_df.shape[0]
(37137, 24759, 20640)
data_train.describe()
| MedInc | HouseAge | AveRooms | AveBedrms | Population | AveOccup | Latitude | Longitude | MedHouseVal | |
|---|---|---|---|---|---|---|---|---|---|
| count | 37137.000000 | 37137.000000 | 37137.000000 | 37137.000000 | 37137.000000 | 37137.000000 | 37137.000000 | 37137.000000 | 37137.000000 |
| mean | 3.851029 | 26.057005 | 5.163124 | 1.062204 | 1660.778919 | 2.831243 | 35.570030 | -119.554329 | 2.079751 |
| std | 1.803167 | 12.158221 | 1.206242 | 0.096490 | 1302.469608 | 2.702413 | 2.083179 | 1.974028 | 1.158571 |
| min | 0.499900 | 2.000000 | 0.851064 | 0.500000 | 3.000000 | 0.950000 | 32.550000 | -124.350000 | 0.149990 |
| 25% | 2.602300 | 17.000000 | 4.357522 | 1.020305 | 952.000000 | 2.394495 | 33.930000 | -121.800000 | 1.208000 |
| 50% | 3.515600 | 25.000000 | 5.068611 | 1.054545 | 1383.000000 | 2.744828 | 34.190000 | -118.450000 | 1.808000 |
| 75% | 4.699700 | 35.000000 | 5.858597 | 1.088825 | 1856.000000 | 3.125313 | 37.700000 | -118.020000 | 2.660000 |
| max | 15.000100 | 52.000000 | 28.837607 | 5.873181 | 35682.000000 | 502.990610 | 41.950000 | -114.550000 | 5.000010 |
f, ax= plt.subplots(figsize=(16,12), ncols=2, nrows=2)
ax[0,0].scatter(data_train['Latitude'], data_train['Longitude'], s=1)
h = ax[0,1].hist2d(x=data_train['Latitude'],
y=data_train['Longitude'],
weights=data_train['MedHouseVal'], bins = 500, vmin=0, vmax=100)
plt.colorbar(h[3], ax=ax[0,1])
h = ax[1,0].hist2d(x=data_train['Latitude'],
y=data_train['Longitude'],
weights=data_train['HouseAge'], bins = 1000, vmin=0, vmax=100)
plt.colorbar(h[3], ax=ax[1,0])
h = ax[1,1].hist2d(x=data_train['Latitude'],
y=data_train['Longitude'],
weights=data_train['Population'], bins = 1000, vmin=0, vmax=10000)
plt.colorbar(h[3], ax=ax[1,1])
for i in range(2):
for j in range(2):
ax[i,j].set_xlabel('Latitude'); ax[i,j].set_ylabel('Longitude')
ax[0,0].set_title('House Distribution', fontsize=15)
ax[0,1].set_title('MedHouseVal Distribution', fontsize=15)
ax[1,0].set_title('HouseAge Distribution', fontsize=15)
ax[1,1].set_title('Population Distribution', fontsize=15)
plt.show()
It's alwas important to check your data distribution, that will give you a sense of which variables behave oddly and may require preprocessing. The following helper function will plot the mean value of the target column with the same scale of the generated histplots. It's a quick a fun way to visualize the relationships between the features and the target.
def add_secondary_plot(
df, column, target_column, ax, n_bins, color=3,
show_yticks=False,
):
secondary_ax = ax.twinx()
bins = pd.cut(df[column], bins=n_bins)
bins = pd.IntervalIndex(bins)
bins = (bins.left + bins.right) / 2
target = df.groupby(bins)[target_column].mean()
target.plot(
ax=secondary_ax, linestyle='',
marker='.', color=colors[color], label=f'Mean {target_column}'
)
secondary_ax.grid(visible=False)
if not show_yticks:
secondary_ax.get_yaxis().set_ticks([])
return secondary_ax
Insights:
import matplotlib.colors as mpl_colors
def hex_to_rgb(h):
h = h.lstrip('#')
return tuple(int(h[i:i+2], 16)/255 for i in (0, 2, 4))
palette = ['#b4d2b1', '#568f8b', '#1d4a60', '#cd7e59', '#ddb247', '#d15252']
palette_rgb = [hex_to_rgb(x) for x in palette]
cmap = mpl_colors.ListedColormap(palette_rgb)
colors = cmap.colors
bg_color= '#fdfcf6'
n_bins = 50
histplot_hyperparams = {
'kde':True,
'alpha':0.4,
'stat':'percent',
'bins':n_bins
}
columns = features
fig, ax = plt.subplots(2, 4, figsize=(16, 10))
ax = ax.flatten()
for i, column in enumerate(columns):
plot_axes = [ax[i]]
sns.histplot(data_train[column], label='Train',
ax=ax[i], color=colors[0], **histplot_hyperparams)
sns.histplot(data_test[column], label='Test',
ax=ax[i], color=colors[1], **histplot_hyperparams)
# Secondary axis to show mean of target
ax2 = add_secondary_plot(data_train, column, 'MedHouseVal', ax[i], n_bins, color=4)
# titles
ax[i].set_title(f'{column} Distribution');
ax[i].set_xlabel(None)
# remove axes to show only one at the end
plot_axes = [ax[i], ax2]
handles = []
labels = []
for plot_ax in plot_axes:
handles += plot_ax.get_legend_handles_labels()[0]
labels += plot_ax.get_legend_handles_labels()[1]
plot_ax.legend().remove()
fig.legend(handles, labels, loc='upper center', bbox_to_anchor=(0.5, 1.04), fontsize=14, ncol=3)
plt.tight_layout()
data_train.corr()['MedHouseVal'].sort_values(ascending=False)
MedHouseVal 1.000000 MedInc 0.701925 AveRooms 0.366727 HouseAge 0.103210 Population -0.038479 AveOccup -0.048475 Longitude -0.056742 AveBedrms -0.067487 Latitude -0.116499 Name: MedHouseVal, dtype: float64
sns.heatmap(data_train.corr(), cmap='viridis')
<AxesSubplot:>
Even though correlations will give us a clear picture of linear relationships, it's also important to plot distribution to discover non relationships between variables and the target
Insights:
MedInc the greater the MedHouseVal, validated from correlations and from bivariate distribution, a similar effect can be seen on AveRoooms but is less evident.histplot_hyperparams = {
'kde':True,
'alpha':0.4,
'stat':'percent'
}
columns = features
fig, ax = plt.subplots(2, 4, figsize=(16, 10), sharey=True)
ax = ax.flatten()
for i, column in enumerate(columns):
sns.histplot(x=data_train[column],
y=data_train['MedHouseVal'], label='Train', ax=ax[i], color=colors[0], **histplot_hyperparams)
ax[i].set_title(f'{column} Distribution vs MedHouseVal', fontsize=10);
ax[i].set_xlabel(None)
ax[i].legend()
plt.tight_layout()
Insights
import folium
from folium import plugins
from folium.plugins import HeatMap
heat_map = folium.Map(data_train[['Latitude', 'Longitude']].mean(axis=0), zoom_start = 6)
lat_long_list = [[row['Latitude'],row['Longitude']] for index, row in data_train.iterrows()]
HeatMap(lat_long_list, radius=10).add_to(heat_map)
heat_map
import numpy as np # linear algebra
import pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)
from sklearn.model_selection import train_test_split
from sklearn.datasets import fetch_california_housing
!pip install reverse_geocoder
Collecting reverse_geocoder
Downloading reverse_geocoder-1.5.1.tar.gz (2.2 MB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 2.2/2.2 MB 4.0 MB/s eta 0:00:00a 0:00:01
Preparing metadata (setup.py) ... done
Requirement already satisfied: numpy>=1.11.0 in /opt/conda/lib/python3.7/site-packages (from reverse_geocoder) (1.21.6)
Requirement already satisfied: scipy>=0.17.1 in /opt/conda/lib/python3.7/site-packages (from reverse_geocoder) (1.7.3)
Building wheels for collected packages: reverse_geocoder
Building wheel for reverse_geocoder (setup.py) ... done
Created wheel for reverse_geocoder: filename=reverse_geocoder-1.5.1-py3-none-any.whl size=2268088 sha256=b0e3bf17d86323672d8f67ba9a7a9f983ed468b8d18140daae75869e63bb79d9
Stored in directory: /root/.cache/pip/wheels/34/6e/70/5423639428a2cac8ea7eb467214a4254b549b381f306a9c790
Successfully built reverse_geocoder
Installing collected packages: reverse_geocoder
Successfully installed reverse_geocoder-1.5.1
WARNING: Running pip as the 'root' user can result in broken permissions and conflicting behaviour with the system package manager. It is recommended to use a virtual environment instead: https://pip.pypa.io/warnings/venv
train_df = pd.read_csv('/kaggle/input/playground-series-s3e1/train.csv')
test_df = pd.read_csv('/kaggle/input/playground-series-s3e1/test.csv')
submission = pd.read_csv('/kaggle/input/playground-series-s3e1/sample_submission.csv')
train_df = train_df.drop('id', axis=1)
extra_data = fetch_california_housing()
train_data2 = pd.DataFrame(extra_data['data'])
train_data2['MedHouseVal'] = extra_data['target']
train_data2.columns = train_df.columns
train_df['generated'] = 1
test_df['generated'] = 1
train_data2['generated'] = 0
# train_df = pd.concat([train_df, train_data2],axis=0).drop_duplicates()
train_df = pd.concat([train_df, train_data2],axis=0, ignore_index=True)
train_df.loc[33228,['Latitude','Longitude']] = [32.74, -117]
train_df.loc[34363,['Latitude','Longitude']] = [32.71, -117]
train_df.loc[20991,['Latitude','Longitude']] = [34.2, -119]
print(train_df.shape)
train_df.head()
(57777, 10)
| MedInc | HouseAge | AveRooms | AveBedrms | Population | AveOccup | Latitude | Longitude | MedHouseVal | generated | |
|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 2.3859 | 15.0 | 3.827160 | 1.112100 | 1280.0 | 2.486989 | 34.60 | -120.12 | 0.980 | 1 |
| 1 | 3.7188 | 17.0 | 6.013373 | 1.054217 | 1504.0 | 3.813084 | 38.69 | -121.22 | 0.946 | 1 |
| 2 | 4.7750 | 27.0 | 6.535604 | 1.103175 | 1061.0 | 2.464602 | 34.71 | -120.45 | 1.576 | 1 |
| 3 | 2.4138 | 16.0 | 3.350203 | 0.965432 | 1255.0 | 2.089286 | 32.66 | -117.09 | 1.336 | 1 |
| 4 | 3.7500 | 52.0 | 4.284404 | 1.069246 | 1793.0 | 1.604790 | 37.80 | -122.41 | 4.500 | 1 |
train_df['r'] = np.sqrt(train_df['Latitude']**2 + train_df['Longitude']**2)
train_df['theta'] = np.arctan2(train_df['Latitude'], train_df['Longitude'])
test_df['r'] = np.sqrt(test_df['Latitude']**2 + test_df['Longitude']**2)
test_df['theta'] = np.arctan2(test_df['Latitude'], test_df['Longitude'])
df = pd.concat([train_df, test_df], axis=0, ignore_index=True)
Encoding trick (see here: https://www.kaggle.com/competitions/playground-series-s3e1/discussion/376210)
emb_size = 20
precision = 1e6
latlon = np.expand_dims(df[['Latitude', 'Longitude']].values, axis=-1)
m = np.exp(np.log(precision) / emb_size)
angle_freq = m ** np.arange(emb_size)
angle_freq = angle_freq.reshape(1, 1, emb_size)
latlon = latlon * angle_freq
latlon[..., 0::2] = np.cos(latlon[..., 0::2])
latlon[..., 1::2] = np.sin(latlon[..., 1::2])
latlon = latlon.reshape(-1, 2 * emb_size)
df['exp_latlon1'] = [lat[0] for lat in latlon]
df['exp_latlon2'] = [lat[1] for lat in latlon]
Using feature engineering ideas with coordinates suggested here: https://www.kaggle.com/code/dmitryuarov/ps-s3e1-coordinates-key-to-victory
from sklearn.decomposition import PCA
def pca(data):
'''
input: dataframe containing Latitude(x) and Longitude(y)
'''
coordinates = data[['Latitude','Latitude']].values
pca_obj = PCA().fit(coordinates)
pca_x = pca_obj.transform(data[['Latitude', 'Longitude']].values)[:,0]
pca_y = pca_obj.transform(data[['Latitude', 'Longitude']].values)[:,1]
return pca_x, pca_y
# train_df['pca_x'], train_df['pca_y'] = pca(train_df)
# test_df['pca_x'], test_df['pca_y'] = pca(test_df)
df['pca_x'], df['pca_y'] = pca(df)
from umap import UMAP
# UMAP: Uniform Manifold Approximation and Projection for Dimension Reduction
coordinates = df[['Latitude', 'Longitude']].values
umap = UMAP(n_components=2, n_neighbors=50, random_state=228).fit(coordinates)
df['umap_lat'] = umap.transform(coordinates)[:,0]
df['umap_lon'] = umap.transform(coordinates)[:,1]
def crt_crds(df):
df['rot_15_x'] = (np.cos(np.radians(15)) * df['Longitude']) + \
(np.sin(np.radians(15)) * df['Latitude'])
df['rot_15_y'] = (np.cos(np.radians(15)) * df['Latitude']) + \
(np.sin(np.radians(15)) * df['Longitude'])
df['rot_30_x'] = (np.cos(np.radians(30)) * df['Longitude']) + \
(np.sin(np.radians(30)) * df['Latitude'])
df['rot_30_y'] = (np.cos(np.radians(30)) * df['Latitude']) + \
(np.sin(np.radians(30)) * df['Longitude'])
df['rot_45_x'] = (np.cos(np.radians(45)) * df['Longitude']) + \
(np.sin(np.radians(45)) * df['Latitude'])
return df
# train_df = crt_crds(train_df)
# test_df = crt_crds(test_df)
df = crt_crds(df)
import reverse_geocoder as rg
from sklearn.preprocessing import LabelEncoder
def geocoder(df):
coordinates = list(zip(df['Latitude'], df['Longitude']))
results = rg.search(coordinates)
return results
# results = geocoder(train_df)
# train_df['place'] = [x['admin2'] for x in results]
# results = geocoder(test_df)
# test_df['place'] = [x['admin2'] for x in results]
results = geocoder(df)
df['place'] = [x['admin2'] for x in results]
places = ['Los Angeles County', 'Orange County', 'Kern County',
'Alameda County', 'San Francisco County', 'Ventura County',
'Santa Clara County', 'Fresno County', 'Santa Barbara County',
'Contra Costa County', 'Yolo County', 'Monterey County',
'Riverside County', 'Napa County']
def replace(x):
if x in places:
return x
else:
return 'Other'
# train_df['place'] = train_df['place'].apply(lambda x: replace(x))
# test_df['place'] = test_df['place'].apply(lambda x: replace(x))
df['place'] = df['place'].apply(lambda x: replace(x))
# le = LabelEncoder()
# train_df['place'] = le.fit_transform(train_df['place'])
# test_df['place'] = le.transform(test_df['place'])
# test_df = pd.get_dummies(test_df)
# train_df = pd.get_dummies(train_df)
df = pd.get_dummies(df)
Loading formatted geocoded file...
Distances to cities and coast points
from haversine import haversine
Sac = (38.576931, -121.494949)
SF = (37.780080, -122.420160)
SJ = (37.334789, -121.888138)
LA = (34.052235, -118.243683)
SD = (32.715759, -117.163818)
df['dist_Sac'] = df.apply(lambda x: haversine((x['Latitude'], x['Longitude']), Sac, unit='ft'), axis=1)
df['dist_SF'] = df.apply(lambda x: haversine((x['Latitude'], x['Longitude']), SF, unit='ft'), axis=1)
df['dist_SJ'] = df.apply(lambda x: haversine((x['Latitude'], x['Longitude']), SJ, unit='ft'), axis=1)
df['dist_LA'] = df.apply(lambda x: haversine((x['Latitude'], x['Longitude']), LA, unit='ft'), axis=1)
df['dist_SD'] = df.apply(lambda x: haversine((x['Latitude'], x['Longitude']), SD, unit='ft'), axis=1)
df['dist_nearest_city'] = df[['dist_Sac', 'dist_SF', 'dist_SJ',
'dist_LA', 'dist_SD']].min(axis=1)
from shapely.geometry import LineString, Point
coast_points = LineString([(32.6644, -117.1613), (33.2064, -117.3831),
(33.7772, -118.2024), (34.4634, -120.0144),
(35.4273, -120.8819), (35.9284, -121.4892),
(36.9827, -122.0289), (37.6114, -122.4916),
(38.3556, -123.0603), (39.7926, -123.8217),
(40.7997, -124.1881), (41.7558, -124.1976)])
df['dist_to_coast'] = df.apply(lambda x: Point(x['Latitude'], x['Longitude']).distance(coast_points), axis=1)
# combine latitude and longitude
# codes from
# https://datascience.stackexchange.com/questions/49553/combining-latitude-longitude-position-into-single-feature
from math import radians, cos, sin, asin, sqrt
def single_pt_haversine(lat, lng, degrees=True):
"""
'Single-point' Haversine: Calculates the great circle distance
between a point on Earth and the (0, 0) lat-long coordinate
"""
r = 6371 # Earth's radius (km). Have r = 3956 if you want miles
# Convert decimal degrees to radians
if degrees:
lat, lng = map(radians, [lat, lng])
# 'Single-point' Haversine formula
a = sin(lat/2)**2 + cos(lat) * sin(lng/2)**2
d = 2 * r * asin(sqrt(a))
return d
# add more metric
# referred to this discussion
# https://www.kaggle.com/competitions/playground-series-s3e1/discussion/376210
def manhattan(lat,lng):
return np.abs(lat) + np.abs(lng)
def euclidean(lat,lng):
return (lat**2 + lng**2) **0.5
def add_combine(df):
df['haversine'] = [single_pt_haversine(x, y) for x, y in zip(df.Latitude, df.Longitude)]
df['manhattan'] = [manhattan(x,y) for x,y in zip(df.Latitude, df.Longitude)]
df['euclidean'] = [euclidean(x,y) for x,y in zip(df.Latitude,df.Longitude)]
return df
df = add_combine(df)
train_df = df.iloc[:-len(test_df),:]
test_df = df.iloc[-len(test_df):,:].drop('MedHouseVal', axis=1).reset_index(drop=True)
X = train_df.drop(['MedHouseVal', 'id'], axis=1)
y = train_df.MedHouseVal
X_test = test_df.drop('id', axis=1)
import catboost
from sklearn.model_selection import KFold
from sklearn.metrics import mean_squared_error
n_folds = 15
MAX_ITER = 15000
PATIENCE = 1000
DISPLAY_FREQ = 100
eval_predsCB = []
predsCB = []
k_fold = KFold(n_splits=n_folds, random_state=42, shuffle=True)
MODEL_PARAMS = {
'random_seed': 1234,
# 'learning_rate': 0.1, # 0.15: 0.5678, 0.12: 0.5685, 0.1: 0.56757, 0.05: 0.57, 0.01, 0.57
'iterations': MAX_ITER,
'early_stopping_rounds': PATIENCE,
# 'metric_period': DISPLAY_FREQ,
'use_best_model': True,
'eval_metric': 'RMSE',
'verbose': 1000,
# 'task_type': 'GPU'
}
for train_index, test_index in k_fold.split(X, y):
X_train, X_valid = X.iloc[train_index], X.iloc[test_index]
y_train, y_valid = y.iloc[train_index], y.iloc[test_index]
model = catboost.CatBoostRegressor(**MODEL_PARAMS)
model.fit(X=X_train, y=y_train,
eval_set=[(X_valid, y_valid)],
early_stopping_rounds = PATIENCE,
# metric_period = DISPLAY_FREQ
)
predsCB.append(model.predict(X_test))
# eval_predsCB.append(model.predict(X))
# print("RMSE valid = {}".format(mean_squared_error(y_valid, model.predict(X_valid))))
# print("RMSE full = {}".format(mean_squared_error(y, model.predict(X))))
from xgboost import XGBRegressor
# n_folds = 20
k_fold = KFold(n_splits=n_folds, random_state=42, shuffle=True)
eval_predsXB = []
predsXB = []
PATIENCE = 200
MODEL_PARAMS = { 'n_estimators': 1000, #1000, 5000
# 'learning_rate': 0.05,
'max_depth': 4, # 3
'colsample_bytree': 0.9, # 0.95
'subsample': 1,
'reg_lambda': 20,
'early_stopping_rounds': PATIENCE,
# 'tree_method': 'gpu_hist',
'seed': 1
}
for train_index, test_index in k_fold.split(X, y):
X_train, X_valid = X.iloc[train_index], X.iloc[test_index]
y_train, y_valid = y.iloc[train_index], y.iloc[test_index]
model = XGBRegressor(**MODEL_PARAMS)
model.fit(X=X_train, y=y_train,
eval_set=[(X_valid, y_valid)],
# early_stopping_rounds = PATIENCE,
verbose = 100
)
predsXB.append(model.predict(X_test))
# eval_predsXB.append(model.predict(X))
import lightgbm as lgbm
from lightgbm.sklearn import LGBMRegressor
# n_folds = 20
k_fold = KFold(n_splits=n_folds, random_state=42, shuffle=True)
eval_predsLB = []
predsLB = []
MODEL_PARAMS = {
'learning_rate': 0.01,
'max_depth': 9,
'num_leaves': 90,
'colsample_bytree': 0.8,
'subsample': 0.9,
'subsample_freq': 5,
'min_child_samples': 36,
'reg_lambda': 28,
'n_estimators': 20000,
'metric': 'rmse',
'random_state': 1
}
callbacks = [lgbm.early_stopping(30, verbose=1), lgbm.log_evaluation(period=0)]
for train_index, test_index in k_fold.split(X, y):
X_train, X_valid = X.iloc[train_index], X.iloc[test_index]
y_train, y_valid = y.iloc[train_index], y.iloc[test_index]
model = lgbm.LGBMRegressor(**MODEL_PARAMS)
model.fit(X=X_train, y=y_train,
eval_set=[(X_valid, y_valid)],
# early_stopping_rounds = PATIENCE,
callbacks=callbacks
)
predsLB.append(model.predict(X_test))
# eval_predsLB.append(model.predict(X))
a = 0.4
b = 0.2
c = 0.4
predCB = np.average(np.array(predsCB),axis=0)
predXB = np.average(np.array(predsXB),axis=0)
predLB = np.average(np.array(predsLB),axis=0)
pred = predCB * a + predXB * b + predLB * c
submission['MedHouseVal'] = pred
submission
vals = train_df['MedHouseVal'].unique().tolist()
submission['MedHouseVal'] = submission['MedHouseVal'].apply(lambda x: min(vals, key=lambda v: abs(v - x)))
submission.MedHouseVal.clip(0, 5, inplace=True)
submission.to_csv('submission_v3.1.4.csv', index=False)