{light_pred}
University of Bologna
1
Smart Home Dataset Research & Selection
2
Exploratory Data Analysis (EDA) & Quality Assessment
3
Definition of Methodological Approaches
5
Advanced Modeling & Data Refinement strategies
4
Baseline Implementation: Probabilistic Nowcasting
6
Performance Evaluation & Discussion of Limitations
Project Roadmap
The Dataset

- Raw dataset sourced from UCI ML Repository: smart home sensor stream (house csh114).
- 11.7 million events spanning from June 2011 to May 2017.
- Sensors:
- Lights
- Motion detectors
- Doors
- Buttons
- Light sensors
- Temperature
# DATASET
The sensors
-
Motion Sensors đ˘Â (useful)
- âPIR motion detectors with light sensors that detect presence and ambient brightness (0-100 range)
-
Door Sensors đ˘ (useful)
- Magnetic reed switches that report OPEN/CLOSE states for doors and cabinets
-
Temperature Sensors đ (not informative)
- Built into door packages, measure ambient temperature with 0.5°C accuracy
-
Light Switches đ˘ (useful)
- Report brightness levels (0-100) and capture dimmer adjustment
-
ââLight Sensors đĄ (proxy)
- Photocell sensors that measure ambient brightness levels (0-100 range)
-
Button Sensors đĄ (proxy)
- Capture user interactions with switches including taps, holds, and tap counts
-
Battery Monitors đ´ (independent)
- Track power levels (0-100%) for all battery-operated sensor
# DATASET
Loading Datataset
home = 'csh114'
path_home = root_data + '/' + home + '.rawdata.txt'
df = pd.read_csv(path_home, sep='\t', header=None, names=col_names)
df = df[df['Timestamp'] <= '2014-11-18']
df
# PRESENTING CODE
Time filtering
home = 'csh114'
path_home = root_data + '/' + home + '.rawdata.txt'
df = pd.read_csv(path_home, sep='\t', header=None, names=col_names)
df = df[df['Timestamp'] <= '2014-11-18']
df
# PRESENTING CODE
Raw Dataset
home = 'csh114'
path_home = root_data + '/' + home + '.rawdata.txt'
df = pd.read_csv(path_home, sep='\t', header=None, names=col_names)
df = df[df['Timestamp'] <= '2014-11-18']
df
# PRESENTING CODE

Data Cleaning
def normalize_message(msg):
if msg == 'OPEN':
return 1
elif msg == 'CLOSE':
return 0
elif msg == 'ON':
return 1
elif msg == 'OFF':
return 0
try:
val = float(msg)
return 1 if val > 0 else 0
except (ValueError, TypeError):
return 0
# PRESENTING CODE

# DATA EXPLORATION
Sensors distribution
Comprehensive frequency analysis across all 64 sensors with identification of dominant contributors.Â

Lights and Gaps distributions
Number of total events of each light on the left and average time between light-events.

# DATA EXPLORATION
10 most frequent sensorsÂ
Ten most frequent sensors immediately before light activation.

# DATA EXPLORATION
Previous sensor mean time gap
Average time gap between light activation and previous event overall.

# DATA EXPLORATION
Previous sensor mean time gap
Mean time gap between light activation and previous event for each light.


# DATA EXPLORATION
Same event - Same minute
Average number of sensor-event repetition in the same minute.
# DATA EXPLORATION

Event distribution by Hour
Histogram of events based on hour of the day.

# DATA EXPLORATION
Sample windows
Subset of sensor events in time window of 50.

# DATA EXPLORATION
Dataset Main Problems
All lights in the dataset are underrepresented compared to other sensors, and the âONâ class is heavily imbalanced with respect to âOFFâ, with only about 5% of the data related to the switch being actual ON states.
1. Data Imbalance
All motion sensors generate ON signals at every movement interval, producing many records that are often informative but frequently just noise, making pattern extraction more challenging.
2. Noisy Sensors
Data analysis confirmed with high certainty that there are multiple people in the home, making it much more difficult to track individual âmovementsâ and behavior patterns of single occupants.
3. Multiple Humans


# DATA EXPLORATION
- Kitchen main light
- Significant activity (less noise)
- Known to be correlated to daily routines
- Home Blueprint key position
- Equilibrate activity presence
Our Target (L002)

Prior
L002 Nowcasting
Feature Engineering and Time Crop
pivot = df_lights.pivot_table(index='Timestamp', columns='Sensor', values='Message', aggfunc='last')
pivot_resampled = pivot.resample('1min').max()
df_lights = pivot_resampled.ffill().bfill()
# ...
df_lights['hour_sin'] = np.sin(2 * np.pi * hour / 24)
df_lights['hour_cos'] = np.cos(2 * np.pi * hour / 24)
df_lights['dow_sin'] = np.sin(2 * np.pi * day_of_week / 7)
df_lights['dow_cos'] = np.cos(2 * np.pi * day_of_week / 7)
df_lights['minute_sin'] = np.sin(2 * np.pi * minute / 60)
df_lights['minute_cos'] = np.cos(2 * np.pi * minute / 60)
df_lights['10min_sin'] = np.sin(2 * np.pi * ((minute // 10) % 6) / 6)
df_lights['10min_cos'] = np.cos(2 * np.pi * ((minute // 10) % 6) / 6)
df_lights['month_sin'] = np.sin(2 * np.pi * month / 12)
df_lights['month_cos'] = np.cos(2 * np.pi * month / 12)
# ...
START_TRAIN_TIMESTAMP = '2011-7-01'
END_TEST_TIMESTAMP = '2013-07-01'
df_lights = df_lights[df_lights.index >= START_TRAIN_TIMESTAMP]
df_lights = df_lights[df_lights.index <= END_TEST_TIMESTAMP]
# PRIOR
Train Objective
target = 'L002'
train_ratio = 0.5
feature_cols = ['hour_sin', 'hour_cos', 'dow_sin', 'dow_cos', 'month_sin', 'month_cos', '10min_sin', '10min_cos'] # 'minute_sin', 'minute_cos']
# PyTorch loss function
loss_cross = nn.BCELoss()
def focal_loss(gamma=2., alpha=0.25):
"""Focal loss for PyTorch"""
def focal_loss_fixed(y_pred, y_true):
epsilon = 1e-7
y_pred = torch.clamp(y_pred, epsilon, 1. - epsilon)
cross_entropy = -y_true * torch.log(y_pred) - (1 - y_true) * torch.log(1 - y_pred)
weight = alpha * y_true * torch.pow(1 - y_pred, gamma) + (1 - alpha) * (1 - y_true) * torch.pow(y_pred, gamma)
loss = weight * cross_entropy
return torch.mean(torch.sum(loss, dim=-1))
return focal_loss_fixed
loss_focal = focal_loss(gamma=2., alpha=0.25)
# PRIOR
Train Balancing
class_weights = class_weight.compute_class_weight(
class_weight='balanced',
classes=np.unique(y_train),
y=y_train
)
class_weight_dict = dict(enumerate(class_weights))
print(f"Class weights: {class_weight_dict}")
| Class | Weight |
|---|---|
| 0 (Off) | 0.53 |
| 1 (On) | 7.62 |
# PRIOR
MLP & Train
nn1 = build_nn_model(input_shape=(len(X_train.columns), ),
output_shape=1,
hidden=[128, 128, 64],
output_activation='sigmoid',
backend='pytorch')
train_nn_model(nn1,
X_train,
y_train,
loss=loss_cross,
epochs=20,
verbose=1,
batch_size=64,
backend='pytorch',
class_weight=class_weight_dict)

# PRIOR
Results (Overall)
| Class | Precision | Recall | F1-score | Support |
|---|---|---|---|---|
| 0 | 0.97 | 0.91 | 0.94 | 491828 |
| 1 | 0.30 | 0.55 | 0.39 | 34492 |
# PRIOR
Training Set
Test Set
| Class | Precision | Recall | F1-score | Support |
|---|---|---|---|---|
| 0 | 0.94 | 0.88 | 0.91 | 490066 |
| 1 | 0.13 | 0.22 | 0.16 | 36255 |
Results (daytime 8am - 1am)
# PRIOR
Training Set
| Class | Precision | Recall | F1-score | Support |
|---|---|---|---|---|
| 0 | 0.92 | 0.85 | 0.88 | 339270 |
| 1 | 0.14 | 0.24 | 0.17 | 33751 |
Test Set
| Class | Precision | Recall | F1-score | Support |
|---|---|---|---|---|
| 0 | 0.95 | 0.88 | 0.91 | 340038 |
| 1 | 0.30 | 0.54 | 0.39 | 32562 |
Training Set
# PRIOR

Precision-Recall CurvesÂ
Test Set

Training Set
# PRIOR
Test Set
Results - One Week


Training Set
# PRIOR
Test Set
Results - Morning


Prior - Limitations
In the following larger models the Prior model demonstrated limited utility as an auxiliary feature due to redundancy with learned temporal representations.
However, it provided crucial value as an interpretable probabilistic baseline for identifying time-of-day bias in light activation patterns, enabling better understanding of household behavior.
# REMARKS
Pattern Mining
Idea
Â
We hypothesized that rule-based mining could provide a baseline for comparison and highlight the most meaningful patterns for light prediction.
Using the Apriori algorithm with metric-based filtering, we extracted a compact ruleset (<40 rules) with strong statistical significance.
# PATTERN MINING

Data structure and Time Crop
START_TIMESTAMP = '2011-7-01'
END_TIMESTAMP = '2011-9-01'
DELTA_SAMPLING = '1s'
# ...
df_pm['Message'] = df_pm['Message'].apply(util_rul.normalize_message)
# ...
for sensor_group, sensor_name in zip(groups, group_names):
# ...
df = df[(df['Timestamp'] >= start_timestamp) & (df['Timestamp'] <= end_timestamp)]
# ...
pivot = df_group.pivot_table(index='Timestamp', columns='Sensor', values='Message', aggfunc='last')
# ...
pivot_resampled = pivot.resample(delta_sampling).max()
pivot_resampled = pivot_resampled.ffill().bfill()
pivot_dfs.append(pivot_resampled)
# ...
pivot_df = reduce(lambda left, right: left.join(right, how='outer'), pivot_dfs)
pivot_df = pivot_df.ffill().bfill()
# Keep only rows that are different from the previous row (remove consecutive duplicates)
mask = (df_resampled != df_resampled.shift()).any(axis=1)
df_resampled = df_resampled[mask]
# PATTERN MINING
Apriori - Search Item-sets
frequent_itemsets = apriori(df_train,
min_support=0.005,
use_colnames=True,
max_len=10,
verbose=1,
low_memory=False )
# PATTERN MINING
 Itemsets containing L002: 2735

Association rules
rules = association_rules(frequent_itemsets,
metric="support",
min_threshold=0.005,
support_only=False)
# PATTERN MINING
Number of metric-filtered rules predicting L002 ON: 39

Evaluation
# PATTERN MINING
Rules were evaluated by sliding through the dataset and checking if recent events matched any rule with L002 âOnâ as consequence. Matches yielded positive predictions; non-matches yielded 0.

L005 Â (Living room - Light)
M005 (Corridor - Motion)
L002 Â (Kitchen - Light)
Results
| Class | Precision | Recall | F1-score | Support |
|---|---|---|---|---|
| 0 | 0.41 | 1.00 | 0.59 | 35773 |
| 1 | 0.96 | 0.05 | 0.10 | 52990 |
Training Set
Test Set
| Class | Precision | Recall | F1-score | Support |
|---|---|---|---|---|
| 0 | 0.44 | 0.99 | 0.61 | 39188 |
| 1 | 0.54 | 0.01 | 0.02 | 49576 |
# PATTERN MINING
Rules Analysis
# PATTERN MINING
Top Rules (for L002):
- L004, L008, M001 âL002
- L006, L003, L008 Â âL002
- L007, L003, D002 âL002

High-frequency sensors L005 (living room light) and M005 (corridor motion) demonstrate poor predictive signal-to-noise ratio despite spatial importance, indicating noise sensitivity in rule extraction thresholds.
Forecasting
- RUL prediction
- Light state prediction
- Switch prediction
- Turn on prediction
Task 1
RUL prediction
Idea - RUL Pred.
Â
Predict time until the next light activation with graphical display to users.
Recommend proactive activation when predictions fall within a tunable threshold (e.g., 5 minutes), enabling post-training threshold optimization for practical deployment.
# RUL PREDICTION

Target Creation
target = 'L002'
values = pivot_df_resampled[target].astype(int).values
time_to_on = np.zeros_like(values)
next_on = -1
for i in range(len(values) - 1, -1, -1):
if values[i] == 1:
next_on = 0
time_to_on[i] = 0
elif next_on == -1:
time_to_on[i] = 0
else:
next_on += 1
time_to_on[i] = next_on
pivot_df_resampled['time_to_on'] = time_to_on
pivot_df_resampled = pivot_df_resampled[pivot_df_resampled['time_to_on'] < 1000]
MAX_TIME_TO_ON = pivot_df_resampled['time_to_on'].max()
pivot_df_resampled['time_to_on'] = pivot_df_resampled['time_to_on'] / MAX_TIME_TO_ON
# RUL PREDICTION
Regression Tree
# Regression tree
reg = DecisionTreeRegressor(max_depth=8, random_state=42)
reg.fit(X_train, y_train)
# Predict
y_pred = reg.predict(X_test)
# RUL PREDICTION
# Filter the values of y_test where the ground truth is between 1 and 6
mask = (y_test*MAX_TIME_TO_ON >= 1) & (y_test*MAX_TIME_TO_ON <= 6)
y_test_filtered = y_test[mask]*MAX_TIME_TO_ON
y_pred_filtered = y_pred[mask]*MAX_TIME_TO_ON
mae_filtered = mean_absolute_error(y_test_filtered, y_pred_filtered)
r2_filtered = r2_score(y_test_filtered, y_pred_filtered)MAE: 93.2 Â Â Â
R2: 0.50
MAE: 88.9 Â Â Â
R2: 0.22
Original
Filtered (max 6min before)
MLP & Train
nn2 = util_rul.build_nn_model(input_shape=(len(X_train.columns), ),
output_shape=1,
hidden=[32, 32])
history = util_rul.train_nn_model(nn2,
X_train,
y_train,
validation_split=0.2,
loss='mse',
epochs=30,
batch_size=32,
verbose=1)
# RUL PREDICTION

[ MLP ] Prediction-Target Plot
# RUL PREDICTION
Text

[ MLP ] Results on Test set
# RUL PREDICTION

RUL - Limitations
# RUL PREDICTION
- The RUL model demonstrates strong nocturnal pattern learning, achieving good precision from night to morning, but exhibits poor daytime performance with substantial false positives.
Â
- The fundamental challenge stems from high intra-day variability: activations occur in rapid succession or hours apart.
Task 2
Light state prediction
Idea - State Pred.
Â
Predict which state the light will be in during the next successive time unit.
The idea was to learn which âactivitiesâ in the home the light state was associated with, and exploit this to understand when it should be on and when it should be off, by modeling the light state throughout the entire time range.
Predicted probabilities above a threshold could serve as a continuous control signal for adaptive light dimming levels.
# LIGHT STATE PREDICTION



0%
100%
50%
Sequence Creation
DELTA_SAMPLING = '1min'
DELTA_FORECAST = 1
# ...
df['Message'] = df['Message'].apply(util_rul.normalize_message)
# ...
START_TRAIN_TIMESTAMP = '2011-7-01'
END_TEST_TIMESTAMP = '2013-07-01'
df_lights = df_lights[df_lights.index >= START_TRAIN_TIMESTAMP]
df_lights = df_lights[df_lights.index <= END_TEST_TIMESTAMP]
# ...
def create_sequences(X, y, n_timesteps=4):
Xs, ys = [], []
for i in tqdm(range(len(X) - n_timesteps + 1)):
Xs.append(X[i:i+n_timesteps])
ys.append(y[i+n_timesteps-1])
return np.array(Xs), np.array(ys)
# Create LSTM sequences
X_train_lstm, y_train_lstm = create_sequences(X_train.values,
y_train.values,
n_timesteps=n_timesteps)
X_test_lstm, y_test_lstm = create_sequences(X_test.values,
y_test.values,
n_timesteps=n_timesteps)
# LIGHT STATE PREDICTION
Class Weight
class_weights = class_weight.compute_class_weight(
'balanced',
classes=np.unique(y_train_lstm),
y=y_train_lstm
)
class_weight_dict = {i: class_weights[i] for i in range(len(class_weights))}
# LIGHT STATE PREDICTION
| Class | Weight |
|---|---|
| 0 (Off) | 0.71 |
| 1 (On) | 1.68 |
Model - LSTMÂ
model = Sequential([
Input(shape=(X_train_lstm.shape[1], X_train_lstm.shape[2])),
LSTM(256),
Dense(64, activation='relu'),
Dense(1, activation='sigmoid')
])
loss_cross = keras.losses.BinaryFocalCrossentropy(
apply_class_balancing=False,
alpha=0.25,
gamma=2.0,
from_logits=False,
label_smoothing=0.1,
axis=-1,
reduction="sum_over_batch_size",
name="binary_focal_crossentropy",
dtype=None,
)
opt = keras.optimizers.Adam(learning_rate=0.001)
model.compile(optimizer=opt, loss=loss_cross)
# LIGHT STATE PREDICTION
Train
history = model.fit(
X_train_lstm, y_train_lstm,
epochs=10,
batch_size=256,
validation_data=(X_test_lstm, y_test_lstm),
verbose=1,
shuffle=True,
class_weight=class_weight_dict
)
# LIGHT STATE PREDICTION

ResultsÂ
| Class | Precision | Recall | F1-score | Support |
|---|---|---|---|---|
| 0 | 0.97 | 1.00 | 0.99 | 369874 |
| 1 | 0.99 | 0.94 | 0.97 | 156436 |
Training Set
Test Set
# LIGHT STATE PREDICTION
| Class | Precision | Recall | F1-score | Support |
|---|---|---|---|---|
| 0 | 0.98 | 1.00 | 0.99 | 395476 |
| 1 | 0.99 | 0.93 | 0.96 | 130835 |
But... All that glitters is not gold
Task formulation induces exploitable shortcut through label stationarity bias.
Post-transition, model predicts prior stable state rather than forecasting upcoming transitions.
State transition events constitute minority class relative to stationary periods, creating severe imbalance that rewards conservative predictions and penalizes transition detection.
# LIGHT STATE PREDICTION


Cost Model - Limitations
- Initially we defined a cost model that evaluates predictions based on temporal proximity to actual events within a defined window, weighting more predictions closer to events.
Â
-
But, defining the cost model is inherently difficult:
- What window is acceptable? (1 min or 1 hour?)
- How to weight different temporal distances?
- How to account for data preprocessing?
- Multiple interdependent design choices lack clear optimal answers, making generalization among tasks challenging.
# REMARKS
Task 3
Switch prediction
Idea - Switch Pred.
Â
Develop a binary classification model for switch event prediction at t+1, identifying state transitions (OFFâON or ONâOFF) as distinct from no-switch time steps.
Captures transition dynamics separately from state maintenance patterns.
# SWITCH PREDICTION

Sequence Creation
start_timestamp = '2011-7-01'
end_timestamp = '2013-07-01'
DELTA_SAMPLING = '1min'
DELTA_FORECAST = 1
# ...
# Create switch target: 1 only at transitions (0->1 or 1->0)
pivot_df_resampled[TARGET] = pivot_df_resampled['L002'].diff().abs().fillna(0)
# SWITCH PREDICTION
State Sequence LSTM
model = Sequential([
Input(shape=(X_train_lstm.shape[1], X_train_lstm.shape[2])),
LSTM(256),
Dense(64, activation='relu'),
Dense(1, activation='sigmoid')
])
loss_cross = keras.losses.BinaryFocalCrossentropy(
apply_class_balancing=False,
alpha=0.25,
gamma=2.0,
from_logits=False,
label_smoothing=0.1,
axis=-1,
reduction="sum_over_batch_size",
name="binary_focal_crossentropy",
dtype=None,
)
opt = keras.optimizers.Adam(learning_rate=0.001)
model.compile(optimizer=opt, loss=loss_cross)
# SWITCH PREDICTION
Weight Balancing
class_weights = class_weight.compute_class_weight(
'balanced',
classes=np.unique(y_train_lstm),
y=y_train_lstm
)
class_weight_dict = {i: class_weights[i] for i in range(len(class_weights))}
# SWITCH PREDICTION
| Class | Weight |
|---|---|
| 0 (Off) | 0.5 |
| 1 (On) | 110.9 |
Train
history = model.fit(
X_train_lstm, y_train_lstm,
epochs=10,
batch_size=256,
validation_data=(X_test_lstm, y_test_lstm),
verbose=1,
shuffle=True,
class_weight=class_weight_dict
)
# SWITCH PREDICTION

ResultsÂ
| Class | Precision | Recall | F1-score | Support |
|---|---|---|---|---|
| 0 | 1.00 | 1.00 | 1.00 | 523937 |
| 1 | 0.27 | 0.02 | 0.04 | 2373 |
Training Set
Test Set
| Class | Precision | Recall | F1-score | Support |
|---|---|---|---|---|
| 0 | 1.00 | 1.00 | 1.00 | 523891 |
| 1 | 0.14 | 0.01 | 0.02 | 2420 |
# SWITCH PREDICTION
Limitations
# SWITCH PREDICTION
We observed that the task remained overly complex: the model had to simultaneously learn discriminative patterns for both OFFâON and ONâOFF transitions.
Â
To address this, we made the strategic decision to further simplify the task by focusing exclusively on turn-on prediction.
Task 4
Turn on prediction
Idea - Turn on Pred.
Â
Simplify the prediction task by focusing exclusively on turn-ON events (OFFâON).
Hypothesize that activation-preceding patterns are more discriminative than general background patterns, improving classifier performance.
# TURN ON PREDICTION

[State] Sequence Creation
start_timestamp = '2011-7-01'
end_timestamp = '2013-07-01'
DELTA_SAMPLING = '1min'
DELTA_FORECAST = 1
# ...
# Create on_only target: 1 only at 0->1 transitions (light turning ON)
pivot_df_resampled[TARGET] = ((pivot_df_resampled['L002'].diff() == 1).astype(int))
# TURN ON PREDICTION
Difference from Switch Predition

State Sequence LSTM
model = Sequential([
Input(shape=(X_train_lstm.shape[1], X_train_lstm.shape[2])),
LSTM(256),
Dense(64, activation='relu'),
Dense(1, activation='sigmoid')
])
loss_cross = keras.losses.BinaryFocalCrossentropy(
apply_class_balancing=False,
alpha=0.25,
gamma=2.0,
from_logits=False,
label_smoothing=0.1,
axis=-1,
reduction="sum_over_batch_size",
name="binary_focal_crossentropy",
dtype=None,
)
opt = keras.optimizers.Adam(learning_rate=0.001)
model.compile(optimizer=opt, loss=loss_cross)
# TURN ON PREDICTION
Weight Balancing
class_weights = class_weight.compute_class_weight(
'balanced',
classes=np.unique(y_train_lstm),
y=y_train_lstm
)
class_weight_dict = {i: class_weights[i] for i in range(len(class_weights))}
| Class | Weight |
|---|---|
| 0 (Off) | 0.5 |
| 1 (On) | 221.9* |
# TURN ON PREDICTION
* double respect to Switch pred. task
ResultsÂ
| Class | Precision | Recall | F1-score | Support |
|---|---|---|---|---|
| 0 | 1.00 | 1.00 | 1.00 | 525129 |
| 1 | 0.18 | 0.22 | 0.19 | 1186 |
Training Set
Test Set
| Class | Precision | Recall | F1-score | Support |
|---|---|---|---|---|
| 0 | 1.00 | 1.00 | 1.00 | 526316 |
| 1 | 0.05 | 0.11 | 0.07 | 1210 |
# TURN ON PREDICTION
Results on Test set AnalysisÂ
# TURN ON PREDICTION


Pred: 9.24đ˘
GT: Â Â 9.35
Pred: 8.29
GT: Â Â 9.35
đĄ
Pred: 9.02đ˘
GT: Â Â 9.15
Pred: 8:30đĄ
GT: Â Â 9:37
Pred: 7:50đĄ
GT: Â Â 9:05
GT: Â Â 9:40đ
[State seq. Turn On] Cost Model
# TURN ON PREDICTION
| Segments | Number | Mean (length) | Sigma (length) | % of total events |
|---|---|---|---|---|
| Correct | 123 | 2.13 | 1.62 | 10% |
| Wrong | 365 | 2.49 | 2.81 | 28% |
| Tot: 488 |
Cost model evaluated on 1260 "Turn-On" events:
Idea - Event Seq.
Â
- Fundamental paradigm shift: rather than maintaining home state as temporal rows, we structure data using individual events as rows.
- This transforms the representation from a time-aligned state sequence (with concurrent events within each time unit) to an event-centric sequence of successive occurrences.
- Correspondingly, inference changes from fixed-interval (per time unit) to event-triggered: predictions occur whenever an event happens, not at regular time intervals.
# TURN ON PREDICTION

[Event] Sequence Creation
sequence_length = 50
embedding_dim = 32
PRED_FORWARD = 2 # minutes
PRED_FORWARD_SECONDS = 30 # seconds
# ...
df_event_log = df_event_log.sort_values("Timestamp").reset_index(drop=True)
df_event_log["event"] = df_event_log["Sensor"].astype(str) + "_" + df_event_log["Message"].astype(str)
unique_events = df_event_log["event"].unique()
# +1 to reserve 0 for padding
event_to_idx = {event: idx + 1 for idx, event in enumerate(unique_events)}
idx_to_event = {idx: event for event, idx in event_to_idx.items()}
# +1 for padding
num_events = len(event_to_idx) + 1
# TURN ON PREDICTION
LSTM - Without Time Embedding
class LSTMModel(nn.Module):
def __init__(self, num_events, embedding_dim, hidden_dim=64, num_layers=2, dropout=0.3):
super(LSTMModel, self).__init__()
self.embedding = nn.Embedding(num_events, embedding_dim, padding_idx=0)
self.lstm = nn.LSTM(embedding_dim, hidden_dim, num_layers,
batch_first=True, dropout=dropout if num_layers > 1 else 0)
self.dropout = nn.Dropout(dropout)
self.fc1 = nn.Linear(hidden_dim, 16)
self.fc2 = nn.Linear(16, 1)
self.relu = nn.ReLU()
self.sigmoid = nn.Sigmoid()
def forward(self, x):
embedded = self.embedding(x)
lstm_out, (h_n, c_n) = self.lstm(embedded)
# Use last hidden state
last_hidden = h_n[-1]
out = self.dropout(last_hidden)
out = self.relu(self.fc1(out))
out = self.dropout(out)
out = self.sigmoid(self.fc2(out))
return out
# TURN ON PREDICTION
Class Weight
class_weight_dict = util_rul.get_class_weights(dataset_train)
pos_weight = torch.tensor([class_weight_dict[1] / class_weight_dict[0]]).to(device)
# ...
criterion = nn.BCEWithLogitsLoss(pos_weight=pos_weight)
# TURN ON PREDICTION
| Class | Weight |
|---|---|
| 0 (Off) | 0.5 |
| 1 (On) | 20.1 |
ResultsÂ
| Class | Precision | Recall | F1-score | Support |
|---|---|---|---|---|
| 0 | 0.99 | 0.84 | 0.91 | 1528454 |
| 1 | 0.09 | 0.65 | 0.16 | 39082 |
Training Set
Test Set
| Class | Precision | Recall | F1-score | Support |
|---|---|---|---|---|
| 0 | 0.98 | 0.84 | 0.90 | 1525122 |
| 1 | 0.08 | 0.53 | 0.14 | 42414 |
# TURN ON PREDICTION
Handling imbalanced and noisy data
# TURN ON PREDICTION
Balancing Strategy
Objective: Create a dataset with a more balanced positive/negative ratio while maintaining data diversity.
- â Partitions samples into positive/negative groups, then creates a balanced dataset by oversampling positives and undersampling negatives to achieve a target ratio. Combines both groups and shuffles to avoid ordering bias.
Â
Denoising Strategy
Problem: Consecutive identical sensor events create redundancy (e.g., 10à same sensor spike)
- Solution: Identifies consecutive runs of identical sensors and compresses series exceeding max consecutive event repeats by keeping first/last events plus uniformly-sampled intermediates. Preserves temporal structure while reducing noise from sensor jitter.
Results Test set - Model Variants
Without Time embedding (previous result)
With Time embedding + Balancing + Denoising
| Class | Precision | Recall | F1-score | Support |
|---|---|---|---|---|
| 0 | 0.97 | 1.00 | 0.99 | 1525122 |
| 1 | 0.42 | 0.03 | 0.05 | 42414 |
# TURN ON PREDICTION
With Time embedding
| Class | Precision | Recall | F1-score | Support |
|---|---|---|---|---|
| 0 | 0.98 | 0.97 | 0.98 | 1525122 |
| 1 | 0.18 | 0.21 | 0.19 | 42414 |
| Class | Precision | Recall | F1-score | Support |
|---|---|---|---|---|
| 0 | 0.98 | 0.84 | 0.90 | 1525122 |
| 1 | 0.08 | 0.53 | 0.14 | 42414 |
Why this Precision-Recall is Good
With Time embedding + Balancing + Denoising
| Class | Precision | Recall | F1-score | Support |
|---|---|---|---|---|
| 0 | 0.97 | 1.00 | 0.99 | 1525122 |
| 1 | 0.42 | 0.03 | 0.05 | 42414 |
# TURN ON PREDICTION
Daily habits that are stable (e.g. morning kitchen light) yield good metrics.
However, many daily activations show heterogeneous inter-day patterns with irregular, unstable dependencies difficult to model.
Strategic choice: ~8 higher-precision predictions daily
(~3.5 correct) rather than many low-confidence predictions.
Analysis and Limitations
# TURN ON PREDICTION



- The training Precision-Recall curve exhibits strong performance (AP=0.894), indicating a tendency toward overfitting.
- However, the test Precision-Recall curve reveals significant performance degradation (AP=0.144), showing that high precision is maintained only on a very limited number of samples.
- The delicate training dynamics observed across epochs stem directly from the original data imbalance and noise, creating optimization challenges.
Results on Test setÂ
# TURN ON PREDICTION


Results on Test setÂ
# TURN ON PREDICTION

Deploy-oriented Tests
# TURN ON PREDICTION
Temporal adaptation (monthly fine-tuning):
- Address distribution shift via incremental retraining on recent data to capture evolving behavioral patterns.
Â
Multi-task learning (multi-label model):
- Joint prediction across all lights leveraging inter-light dependencies rather than independent single-light classifiers for improved whole-home coverage.
Â
But... both strategies require deployment considerations for production-grade performance parity with single-light validation.
{The End}
Thank you for the attention!
Code
By alessandro sciarrillo
Code
- 7