Improve RUL prediction with time-series features
Browse files
batteryswap_example/planners/best.pickle
CHANGED
|
Binary files a/batteryswap_example/planners/best.pickle and b/batteryswap_example/planners/best.pickle differ
|
|
|
batteryswap_example/train.py
CHANGED
|
@@ -8,7 +8,7 @@ import pathlib
|
|
| 8 |
import os
|
| 9 |
|
| 10 |
|
| 11 |
-
from sklearn.
|
| 12 |
import pandas as pd
|
| 13 |
import numpy
|
| 14 |
import numpy as np
|
|
@@ -80,18 +80,127 @@ class DummyRULModel(RULModel):
|
|
| 80 |
self.use_total_elapsed_days = True
|
| 81 |
|
| 82 |
def _compute_features(self, unit_df: pd.DataFrame) -> np.ndarray:
|
| 83 |
-
unit_df = unit_df.
|
|
|
|
|
|
|
| 84 |
|
| 85 |
all_stats = {}
|
| 86 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 87 |
|
| 88 |
feature_names = sorted(all_stats.keys())
|
| 89 |
-
self._feature_names_ = feature_names
|
| 90 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 91 |
|
| 92 |
def _build_feature_matrix(self, timeseries: pd.DataFrame) -> tuple[np.ndarray, list]:
|
| 93 |
rows, ids = [], []
|
| 94 |
-
for unit_id, unit_df in timeseries.groupby(
|
|
|
|
|
|
|
|
|
|
| 95 |
if len(unit_df) < 2:
|
| 96 |
continue
|
| 97 |
rows.append(self._compute_features(unit_df))
|
|
@@ -103,13 +212,21 @@ class DummyRULModel(RULModel):
|
|
| 103 |
y = np.array([rul[unit_id] for unit_id in ids])
|
| 104 |
|
| 105 |
# FIXME: actually use an estimator that learns
|
| 106 |
-
self.model =
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 107 |
self.model.fit(X, y)
|
| 108 |
return self
|
| 109 |
|
| 110 |
def predict(self, timeseries: pd.DataFrame) -> pd.DataFrame:
|
| 111 |
rows, ids = [], []
|
| 112 |
-
for unit_id, unit_df in timeseries.groupby(
|
|
|
|
|
|
|
|
|
|
| 113 |
rows.append(self._compute_features(unit_df))
|
| 114 |
ids.append(unit_id)
|
| 115 |
|
|
|
|
| 8 |
import os
|
| 9 |
|
| 10 |
|
| 11 |
+
from sklearn.ensemble import ExtraTreesRegressor
|
| 12 |
import pandas as pd
|
| 13 |
import numpy
|
| 14 |
import numpy as np
|
|
|
|
| 80 |
self.use_total_elapsed_days = True
|
| 81 |
|
| 82 |
def _compute_features(self, unit_df: pd.DataFrame) -> np.ndarray:
|
| 83 |
+
unit_df = unit_df.sort_index(level=self.time_col).copy()
|
| 84 |
+
if len(unit_df) == 0:
|
| 85 |
+
raise ValueError("Received empty battery time series")
|
| 86 |
|
| 87 |
all_stats = {}
|
| 88 |
+
|
| 89 |
+
# Convert timestamps to elapsed days
|
| 90 |
+
times = pd.to_datetime(
|
| 91 |
+
unit_df.index.get_level_values(self.time_col)
|
| 92 |
+
)
|
| 93 |
+
elapsed_days = (
|
| 94 |
+
times - times[0]
|
| 95 |
+
).total_seconds().to_numpy() / 86400.0
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
# General history features
|
| 99 |
+
all_stats["n_obs"] = float(len(unit_df))
|
| 100 |
+
all_stats["history_days"] = (
|
| 101 |
+
float(elapsed_days[-1]) if len(elapsed_days) > 1 else 0.0
|
| 102 |
+
)
|
| 103 |
+
|
| 104 |
+
# Features for voltage and temperature
|
| 105 |
+
for col in self.value_cols:
|
| 106 |
+
values = pd.to_numeric(
|
| 107 |
+
unit_df[col],
|
| 108 |
+
errors="coerce"
|
| 109 |
+
)
|
| 110 |
+
|
| 111 |
+
valid = values.notna()
|
| 112 |
+
|
| 113 |
+
if valid.sum() == 0:
|
| 114 |
+
all_stats[f"{col}_latest"] = 0.0
|
| 115 |
+
all_stats[f"{col}_mean"] = 0.0
|
| 116 |
+
all_stats[f"{col}_std"] = 0.0
|
| 117 |
+
all_stats[f"{col}_min"] = 0.0
|
| 118 |
+
all_stats[f"{col}_max"] = 0.0
|
| 119 |
+
all_stats[f"{col}_change"] = 0.0
|
| 120 |
+
all_stats[f"{col}_slope"] = 0.0
|
| 121 |
+
continue
|
| 122 |
+
|
| 123 |
+
x = values[valid].to_numpy(dtype=float)
|
| 124 |
+
t = elapsed_days[valid.to_numpy()]
|
| 125 |
+
|
| 126 |
+
all_stats[f"{col}_latest"] = float(x[-1])
|
| 127 |
+
all_stats[f"{col}_mean"] = float(np.mean(x))
|
| 128 |
+
all_stats[f"{col}_std"] = float(np.std(x))
|
| 129 |
+
all_stats[f"{col}_min"] = float(np.min(x))
|
| 130 |
+
all_stats[f"{col}_max"] = float(np.max(x))
|
| 131 |
+
all_stats[f"{col}_change"] = float(x[-1] - x[0])
|
| 132 |
+
|
| 133 |
+
# Recent-window features
|
| 134 |
+
for window_days in (7, 14, 30):
|
| 135 |
+
cutoff = t[-1] - window_days
|
| 136 |
+
recent_mask = t >= cutoff
|
| 137 |
+
|
| 138 |
+
recent_x = x[recent_mask]
|
| 139 |
+
recent_t = t[recent_mask]
|
| 140 |
+
|
| 141 |
+
prefix = f"{col}_{window_days}d"
|
| 142 |
+
|
| 143 |
+
if len(recent_x) > 0:
|
| 144 |
+
all_stats[f"{prefix}_mean"] = float(np.mean(recent_x))
|
| 145 |
+
all_stats[f"{prefix}_std"] = float(np.std(recent_x))
|
| 146 |
+
all_stats[f"{prefix}_min"] = float(np.min(recent_x))
|
| 147 |
+
all_stats[f"{prefix}_max"] = float(np.max(recent_x))
|
| 148 |
+
all_stats[f"{prefix}_change"] = float(
|
| 149 |
+
recent_x[-1] - recent_x[0]
|
| 150 |
+
)
|
| 151 |
+
|
| 152 |
+
if len(recent_x) >= 2 and np.ptp(recent_t) > 0:
|
| 153 |
+
recent_slope = np.polyfit(
|
| 154 |
+
recent_t,
|
| 155 |
+
recent_x,
|
| 156 |
+
1
|
| 157 |
+
)[0]
|
| 158 |
+
else:
|
| 159 |
+
recent_slope = 0.0
|
| 160 |
+
|
| 161 |
+
all_stats[f"{prefix}_slope"] = float(recent_slope)
|
| 162 |
+
else:
|
| 163 |
+
all_stats[f"{prefix}_mean"] = 0.0
|
| 164 |
+
all_stats[f"{prefix}_std"] = 0.0
|
| 165 |
+
all_stats[f"{prefix}_min"] = 0.0
|
| 166 |
+
all_stats[f"{prefix}_max"] = 0.0
|
| 167 |
+
all_stats[f"{prefix}_change"] = 0.0
|
| 168 |
+
all_stats[f"{prefix}_slope"] = 0.0
|
| 169 |
+
|
| 170 |
+
# Recent level compared with overall level
|
| 171 |
+
if len(x) > 0:
|
| 172 |
+
recent_7_mask = t >= (t[-1] - 7)
|
| 173 |
+
recent_7 = x[recent_7_mask]
|
| 174 |
+
|
| 175 |
+
if len(recent_7) > 0:
|
| 176 |
+
all_stats[f"{col}_recent7_vs_mean"] = float(
|
| 177 |
+
np.mean(recent_7) - np.mean(x)
|
| 178 |
+
)
|
| 179 |
+
else:
|
| 180 |
+
all_stats[f"{col}_recent7_vs_mean"] = 0.0
|
| 181 |
+
|
| 182 |
+
# Trend per day
|
| 183 |
+
if len(x) >= 2 and np.ptp(t) > 0:
|
| 184 |
+
slope = np.polyfit(t, x, 1)[0]
|
| 185 |
+
else:
|
| 186 |
+
slope = 0.0
|
| 187 |
+
|
| 188 |
+
all_stats[f"{col}_slope"] = float(slope)
|
| 189 |
|
| 190 |
feature_names = sorted(all_stats.keys())
|
| 191 |
+
self._feature_names_ = feature_names
|
| 192 |
+
|
| 193 |
+
return np.array(
|
| 194 |
+
[all_stats[k] for k in feature_names],
|
| 195 |
+
dtype=float,
|
| 196 |
+
)
|
| 197 |
|
| 198 |
def _build_feature_matrix(self, timeseries: pd.DataFrame) -> tuple[np.ndarray, list]:
|
| 199 |
rows, ids = [], []
|
| 200 |
+
for unit_id, unit_df in timeseries.groupby(
|
| 201 |
+
self.group_col,
|
| 202 |
+
observed=True
|
| 203 |
+
):
|
| 204 |
if len(unit_df) < 2:
|
| 205 |
continue
|
| 206 |
rows.append(self._compute_features(unit_df))
|
|
|
|
| 212 |
y = np.array([rul[unit_id] for unit_id in ids])
|
| 213 |
|
| 214 |
# FIXME: actually use an estimator that learns
|
| 215 |
+
self.model = ExtraTreesRegressor(
|
| 216 |
+
n_estimators=300,
|
| 217 |
+
min_samples_leaf=2,
|
| 218 |
+
random_state=42,
|
| 219 |
+
n_jobs=-1,
|
| 220 |
+
)
|
| 221 |
self.model.fit(X, y)
|
| 222 |
return self
|
| 223 |
|
| 224 |
def predict(self, timeseries: pd.DataFrame) -> pd.DataFrame:
|
| 225 |
rows, ids = [], []
|
| 226 |
+
for unit_id, unit_df in timeseries.groupby(
|
| 227 |
+
self.group_col,
|
| 228 |
+
observed=True
|
| 229 |
+
):
|
| 230 |
rows.append(self._compute_features(unit_df))
|
| 231 |
ids.append(unit_id)
|
| 232 |
|