Estimating Lithium-ion Battery Health¶
Introduction¶
Lithium-based batteries - their cycles characteristics and aging¶
Lithium-ion (or Li-ion) batteries are rechargeable batteries used for a variety of electronic devices, which range from eletric vehicles, smartphones, and even satellites.
However, despite their wide adoption, research isn’t mature enough to avoid problems with battery health and safety, and given the ubiquity of consumer electronics using the technology, this has led to some poor outcomes that range from poor user-experience to public safety concerns (see, for example, the Samsung Galaxy Note 7 explosions from 2016).
Dataset¶
In this example of predictive maintenance, we propose a data-driven method to estimate the health of a battery using the Li-ion battery dataset released by NASA.
This dataset includes information on Li-ion batteries over several charge and discharge cycles at room temperature. Charging was at a constant current (CC) at 1.5A until the battery voltage reached 4.2V and then continued in a constant voltage (CV) mode until the charge current dropped to 20mA.
Discharge was at a constant current (CC) level of 2A until the battery voltage fell to 2.7V.
The dataset includes the following:
Voltage_measured: Battery’s terminal voltage (Volts) for charging and discharging cycles.
Current_measured: Battery’s output current (Amps) for charging and discharging cycles.
Temperature_measured: Battery temperature (degree Celsius).
Current_charge: Current measured at charger for charging cycles and at load for discharging cycles (Amps).
Voltage_charge: Voltage measured at charger for charging cycles and at load for discharging ones (Volts).
Start_time: Starting time of the cycle.
Time: Time in seconds after the starting time for the cycle (seconds).
Capacity: Battery capacity (Ahr) for discharging until 2.7V. Battery capacity is the product of the current drawn from the battery (while the battery is able to supply the load) until its voltage drops lower than a certain value for each cell.
We will follow the data science cycle (Data Exploration - Data Preparation - Data Modeling - Model Evaluation - Model Deployment) to solve this problem.
Initialization¶
This example uses the following version of vastorbit:
import vastorbit as vo
vo.__version__
Connect to VAST. This example uses an existing connection called VASTDSN.
For details on how to create a connection, see the Connection tutorial.
You can skip the below cell if you already have an established connection.
vo.connect("VASTDSN")
Let us now ingest the data.
# load datasets as VastFrame objects
battery5 = vo.read_csv("battery.csv")
# Display
battery5
123 voltage_measuredDouble | 123 current_measuredDouble | 123 temperature_measuredDouble | 123 current_chargeDouble | 123 voltage_chargeDouble | 123 timeDouble | Abc typeVarchar(50) | 📅 start_timeTimestamp(3) | 123 ambient_tempInteger | 123 capacityDouble | |
|---|---|---|---|---|---|---|---|---|---|---|
| 1 | 3.99859467528732 | 1.5086578130392 | 25.3824087259984 | 1.5 | 4.748 | 807.016 | charge | 2008-05-09 17:26:51 | 24 | [null] |
| 2 | 3.99860287917511 | 1.51107860634553 | 26.233612006402 | 1.498 | 4.748 | 871.812 | charge | 2008-05-06 22:46:40 | 24 | [null] |
| 3 | 3.99861412140464 | 1.50946377217514 | 26.4876862500451 | 1.498 | 4.748 | 639.906 | charge | 2008-05-14 06:18:57 | 24 | [null] |
| 4 | 3.99870976854192 | 1.51035063467963 | 27.7624695129062 | 1.5 | 4.753 | 1631.829 | charge | 2008-04-29 13:44:46 | 24 | [null] |
| 5 | 3.99874624314387 | 1.51131424103643 | 26.8713113364232 | 1.5 | 4.748 | 597.594 | charge | 2008-05-15 16:33:24 | 24 | [null] |
| 6 | 3.99904211905689 | 1.50942361985491 | 25.5637616178828 | 1.498 | 4.753 | 1289.64 | charge | 2008-05-01 09:43:18 | 24 | [null] |
| 7 | 3.99908212283509 | 1.5122767901965 | 25.9915427787926 | 1.498 | 4.742 | 379.453 | charge | 2008-05-25 21:43:40 | 24 | [null] |
| 8 | 3.99914045091142 | 1.50782241717979 | 25.6241344580037 | 1.498 | 4.737 | 1428.766 | charge | 2008-04-18 17:34:22 | 24 | [null] |
| 9 | 3.99956104334766 | 1.5147827283378 | 25.8060674345069 | 1.5 | 4.742 | 1628.203 | charge | 2008-04-22 21:56:21 | 24 | [null] |
| 10 | 3.99969752863665 | 1.51109052805073 | 26.9796216685414 | 1.498 | 4.748 | 773.64 | charge | 2008-05-12 20:23:51 | 24 | [null] |
| 11 | 3.99969867900127 | 1.50730505421728 | 26.6752299839961 | 1.5 | 4.748 | 715.985 | charge | 2008-05-13 05:58:50 | 24 | [null] |
| 12 | 3.99976427510953 | 1.51030621289821 | 26.2478228791738 | 1.5 | 4.753 | 1490.594 | charge | 2008-04-29 23:28:20 | 24 | [null] |
| 13 | 3.99991496166186 | 1.51223510485125 | 25.9750322528727 | 1.498 | 4.753 | 1086.0 | charge | 2008-05-03 20:24:21 | 24 | [null] |
| 14 | 3.99992735176979 | 1.51124154608623 | 25.8578252232494 | 1.5 | 4.748 | 1445.922 | charge | 2008-04-24 23:06:34 | 24 | [null] |
| 15 | 4.00013018975126 | 1.50934269494308 | 25.2039694991594 | 1.5 | 4.742 | 1319.813 | charge | 2008-04-28 17:20:47 | 24 | [null] |
| 16 | 4.0001886414272 | 1.51093213415203 | 26.6747147417433 | 1.5 | 4.731 | 1622.719 | charge | 2008-04-02 16:37:51 | 24 | [null] |
| 17 | 4.00020318481464 | 1.50810346655804 | 26.8749785842611 | 1.498 | 4.753 | 607.687 | charge | 2008-05-15 16:33:24 | 24 | [null] |
| 18 | 4.00044592778236 | 1.50905513326467 | 26.3074942111978 | 1.5 | 4.748 | 781.563 | charge | 2008-05-11 08:19:27 | 24 | [null] |
| 19 | 4.00051812975112 | 1.51070065060751 | 24.9761050120483 | 1.5 | 4.748 | 1371.015 | charge | 2008-04-26 07:25:23 | 24 | [null] |
| 20 | 4.00055480506039 | 1.51232891393498 | 25.6376503299239 | 1.5 | 4.748 | 887.219 | charge | 2008-05-06 12:50:20 | 24 | [null] |
Warning
This example uses a sample dataset. For the full analysis, you should consider using the complete dataset.
Understanding the Data¶
Let’s perform a few aggregations with describe() to get a high-level overview of the dataset.
battery5.describe()
| count | mean | std | min | approx_25% | approx_50% | approx_75% | max | |
|---|---|---|---|---|---|---|---|---|
| "voltage_measured" | 35288.0 | 4.102376412895808 | 0.21569281574856955 | 2.59294796477655 | 4.080122793855556 | 4.205098590281656 | 4.205920632302799 | 4.98472045064886 |
| "current_measured" | 35288.0 | 0.36351799870948753 | 0.9102328508247932 | -3.79333527464668 | 0.04227032378889577 | 0.1704399151576974 | 1.157624004166459 | 1.5215190253818 |
| "temperature_measured" | 35288.0 | 26.381913908826345 | 2.801457740981268 | 23.2148017857281 | 24.492422791241268 | 25.490207231220783 | 27.367953670197448 | 41.2725661150757 |
| "current_charge" | 35288.0 | 0.6331409884379983 | 0.7385171402476751 | -3.785 | 0.058523838981117494 | 0.265973340815563 | 1.4965234926374547 | 1.9984 |
| "voltage_charge" | 35288.0 | 4.025864259805034 | 1.2017235362296907 | 0.0 | 4.242644514016201 | 4.30265911706512 | 4.651124958015665 | 5.002 |
| "time" | 35288.0 | 4756.114337310144 | 3138.100878893327 | 0.0 | 1918.8785040406417 | 4486.436431144812 | 7417.263296273874 | 10807.219 |
| "ambient_temp" | 35288.0 | 24.0 | 0.0 | 24.0 | 24.0 | 24.0 | 24.0 | 24.0 |
| "capacity" | 3076.0 | 1.556825365527149 | 0.1820326129751037 | 1.28745252213794 | 1.3893011321356146 | 1.5386375515821014 | 1.7400231626578118 | 1.85648742081816 |
To get a better idea of the changes between each cycle, we look at an aggregation at their start time, duration, and voltage at the beginning and the end of each cycle.
battery5["start_time"].describe()
| value | |
|---|---|
| name | "start_time" |
| dtype | timestamp(3) |
| count | 35215 |
| min | 2008-04-02 13:08:17 |
| max | 2008-05-28 11:09:42 |
To see how the voltage changes during the cycle, we extract the initial and final voltage measurements for each cycle.
battery5.analytic(
func = "first_value",
columns = "Voltage_measured",
by = "start_time",
order_by = {"Time": "asc"},
name = "first_voltage_measured",
)
battery5.analytic(
func = "first_value",
columns = "Voltage_measured",
by = "start_time",
order_by = {"Time": "desc"},
name = "last_voltage_measured",
)
cycling_info = battery5.groupby(
columns = [
"start_time",
"type",
"first_voltage_measured",
"last_voltage_measured",
],
expr = [
"COUNT(*) AS nr_of_measurements",
"MAX(Time) AS cycle_duration",
],
).sort("start_time")
cycling_info["cycle_id"] = "ROW_NUMBER() OVER(ORDER BY start_time)"
cycling_info
📅 start_timeTimestamp(3) | Abc typeVarchar(50) | 123 first_voltage_measuredDouble | 123 last_voltage_measuredDouble | 123 nr_of_measurementsBigint | 123 cycle_durationDouble | 123 cycle_idBigint | |
|---|---|---|---|---|---|---|---|
| 1 | 2008-04-02 13:08:17 | charge | 4.07798650841431 | 4.19138815949231 | 42 | 7561.75 | 1 |
| 2 | 2008-04-02 15:25:41 | discharge | 3.8011216334633 | 3.30494144277741 | 11 | 3072.859 | 2 |
| 3 | 2008-04-02 16:37:51 | charge | 3.46878815957312 | 4.18906184108551 | 68 | 10516.0 | 3 |
| 4 | 2008-04-02 19:43:48 | discharge | 4.1888813802791 | 3.24425218899791 | 9 | 3509.61 | 4 |
| 5 | 2008-04-02 20:55:40 | charge | 3.67907267574697 | 4.18739192845213 | 62 | 10401.937 | 5 |
| 6 | 2008-04-03 00:01:06 | discharge | 4.1881867359913 | 3.32745100986863 | 16 | 3651.641 | 6 |
| 7 | 2008-04-03 01:12:38 | charge | 3.51602938670717 | 4.18805495607423 | 53 | 10397.89 | 7 |
| 8 | 2008-04-03 04:16:37 | discharge | 3.86856497821042 | 2.59294796477655 | 10 | 3309.719 | 8 |
| 9 | 2008-04-03 05:27:49 | charge | 3.61043312629589 | 4.18731533276537 | 62 | 10166.718 | 9 |
| 10 | 2008-04-03 08:33:25 | discharge | 4.18829852476105 | 3.26107394236745 | 13 | 3507.391 | 10 |
| 11 | 2008-04-03 09:44:35 | charge | 3.55031650914983 | 4.18817850943803 | 53 | 10352.141 | 11 |
| 12 | 2008-04-03 12:55:10 | discharge | 3.87853219376342 | 3.26168497902227 | 17 | 3530.0 | 12 |
| 13 | 2008-04-03 14:06:43 | charge | 3.5443859855127 | 4.18860068549777 | 55 | 10683.61 | 13 |
| 14 | 2008-04-03 17:17:16 | discharge | 3.8428952095538 | 3.38445289185396 | 13 | 2917.359 | 14 |
| 15 | 2008-04-03 18:28:47 | charge | 3.70774461049346 | 4.20739149806757 | 49 | 9667.515 | 15 |
| 16 | 2008-04-03 21:28:14 | discharge | 3.86265070833128 | 3.30258172883868 | 9 | 3531.75 | 16 |
| 17 | 2008-04-03 22:38:27 | charge | 3.53993764102806 | 4.18757378400996 | 56 | 9901.391 | 17 |
| 18 | 2008-04-04 01:38:15 | discharge | 4.18744822182588 | 3.2326058701879 | 6 | 3429.047 | 18 |
| 19 | 2008-04-04 02:48:06 | charge | 3.69639276100211 | 4.20692182956778 | 51 | 9370.672 | 19 |
| 20 | 2008-04-04 05:48:08 | discharge | 3.93667569036105 | 2.98209271205363 | 11 | 3231.25 | 20 |
We can see from the “duration” column that charging seems to take a longer time than discharging.
Let’s visualize this trend with an animated graph.
cycling_info.animated_bar(
ts = "start_time",
columns = ["type", "cycle_duration"],
)
The animated graph below shows how the cycles change throughout time. Another way we can verify that charging cycles are longer than discharging cycles is by looking at the average duration of each type of cycle.
cycling_info.bar(
["type"],
method = "avg",
of = "cycle_duration",
)
In general, charging cycles are longer than discharging cycles.
Let’s examine how voltage changes between cycles and their transitions.
cycling_info = cycling_info.groupby(
"type",
[
"MIN(first_voltage_measured) AS min_first_voltage",
"AVG(first_voltage_measured) AS avg_first_voltage",
"MAX(first_voltage_measured) AS max_first_voltage",
"MIN(last_voltage_measured) AS min_last_voltage",
"AVG(last_voltage_measured) AS avg_last_voltage",
"MAX(last_voltage_measured) AS max_last_voltage",
],
)
cycling_info
📅 start_timeTimestamp(3) | Abc typeVarchar(50) | 123 first_voltage_measuredDouble | 123 last_voltage_measuredDouble | 123 nr_of_measurementsBigint | 123 cycle_durationDouble | 123 cycle_idBigint | |
|---|---|---|---|---|---|---|---|
| 1 | 2008-04-02 13:08:17 | charge | 4.07798650841431 | 4.19138815949231 | 42 | 7561.75 | 1 |
| 2 | 2008-04-02 15:25:41 | discharge | 3.8011216334633 | 3.30494144277741 | 11 | 3072.859 | 2 |
| 3 | 2008-04-02 16:37:51 | charge | 3.46878815957312 | 4.18906184108551 | 68 | 10516.0 | 3 |
| 4 | 2008-04-02 19:43:48 | discharge | 4.1888813802791 | 3.24425218899791 | 9 | 3509.61 | 4 |
| 5 | 2008-04-02 20:55:40 | charge | 3.67907267574697 | 4.18739192845213 | 62 | 10401.937 | 5 |
| 6 | 2008-04-03 00:01:06 | discharge | 4.1881867359913 | 3.32745100986863 | 16 | 3651.641 | 6 |
| 7 | 2008-04-03 01:12:38 | charge | 3.51602938670717 | 4.18805495607423 | 53 | 10397.89 | 7 |
| 8 | 2008-04-03 04:16:37 | discharge | 3.86856497821042 | 2.59294796477655 | 10 | 3309.719 | 8 |
| 9 | 2008-04-03 05:27:49 | charge | 3.61043312629589 | 4.18731533276537 | 62 | 10166.718 | 9 |
| 10 | 2008-04-03 08:33:25 | discharge | 4.18829852476105 | 3.26107394236745 | 13 | 3507.391 | 10 |
| 11 | 2008-04-03 09:44:35 | charge | 3.55031650914983 | 4.18817850943803 | 53 | 10352.141 | 11 |
| 12 | 2008-04-03 12:55:10 | discharge | 3.87853219376342 | 3.26168497902227 | 17 | 3530.0 | 12 |
| 13 | 2008-04-03 14:06:43 | charge | 3.5443859855127 | 4.18860068549777 | 55 | 10683.61 | 13 |
| 14 | 2008-04-03 17:17:16 | discharge | 3.8428952095538 | 3.38445289185396 | 13 | 2917.359 | 14 |
| 15 | 2008-04-03 18:28:47 | charge | 3.70774461049346 | 4.20739149806757 | 49 | 9667.515 | 15 |
| 16 | 2008-04-03 21:28:14 | discharge | 3.86265070833128 | 3.30258172883868 | 9 | 3531.75 | 16 |
| 17 | 2008-04-03 22:38:27 | charge | 3.53993764102806 | 4.18757378400996 | 56 | 9901.391 | 17 |
| 18 | 2008-04-04 01:38:15 | discharge | 4.18744822182588 | 3.2326058701879 | 6 | 3429.047 | 18 |
| 19 | 2008-04-04 02:48:06 | charge | 3.69639276100211 | 4.20692182956778 | 51 | 9370.672 | 19 |
| 20 | 2008-04-04 05:48:08 | discharge | 3.93667569036105 | 2.98209271205363 | 11 | 3231.25 | 20 |
From this table, it looks like batteries are charged until they are almost full (4.2V) and discharging doesn’t begin until they are fully charged.
End-of-life (EOL) criteria for batteries is usually defined as when the battery capacity is lower than 70%-80% of its rated capacity. Since the rated capacity by the manufacturer for this battery is 2Ah, this battery is considered EOL when its capacity reaches 2Ah x 70% = 1.4Ah.
Let’s plot the capacity curve of the battery with its smoothed version and observe when it reaches the degradation criteria.
But first we need to perform some preprocessing.
discharging_data = battery5[battery5["type"] == "discharge"]
d_cap = discharging_data[["start_time", "Capacity"]].groupby(["start_time", "Capacity"])
d_cap["discharge_id"] = "ROW_NUMBER() OVER(ORDER BY start_time, Capacity)"
d_cap.rolling(
func = "mean",
columns = "capacity",
window = (-100, -1),
name = "smooth_capacity",
)
📅 start_timeTimestamp(3) | 123 capacityDouble | 123 discharge_idBigint | 123 smooth_capacityDouble | |
|---|---|---|---|---|
| 1 | 2008-04-02 15:25:41 | 1.85648742081816 | 1 | 1.7037028048511649 |
| 2 | 2008-04-02 19:43:48 | 1.84632724971993 | 2 | 1.6889999962164293 |
| 3 | 2008-04-03 00:01:06 | 1.83534919422341 | 3 | 1.6777173563148102 |
| 4 | 2008-04-03 04:16:37 | 1.83526252758211 | 4 | 1.673903743313835 |
| 5 | 2008-04-03 08:33:25 | 1.83464550821204 | 5 | 1.666082957811581 |
| 6 | 2008-04-03 12:55:10 | 1.83566166006755 | 6 | 1.6815293861898084 |
| 7 | 2008-04-03 17:17:16 | 1.83514614292266 | 7 | 1.6700427035222207 |
| 8 | 2008-04-03 21:28:14 | 1.82575679056655 | 8 | 1.6543115073726005 |
| 9 | 2008-04-04 01:38:15 | 1.82477385298913 | 9 | 1.6424005897668672 |
| 10 | 2008-04-04 05:48:08 | 1.82461326849694 | 10 | 1.6342560990568322 |
| 11 | 2008-04-04 09:57:19 | 1.82461955268645 | 11 | 1.638328684567316 |
| 12 | 2008-04-04 17:56:27 | 1.81420193576739 | 12 | 1.6220932106173143 |
| 13 | 2008-04-04 22:01:54 | 1.81375215775491 | 13 | 1.609724546530668 |
| 14 | 2008-04-05 02:20:26 | 1.81344049147358 | 14 | 1.6055029895044595 |
| 15 | 2008-04-05 06:25:01 | 1.80259800363065 | 15 | 1.5756552118443041 |
| 16 | 2008-04-05 10:30:32 | 1.80210690024615 | 16 | 1.5669546758186803 |
| 17 | 2008-04-05 14:34:41 | 1.80257950082621 | 17 | 1.571327916871881 |
| 18 | 2008-04-05 18:39:25 | 1.80306831428341 | 18 | 1.5885740470380634 |
| 19 | 2008-04-05 22:46:35 | 1.8027776247196 | 19 | 1.5842986349411237 |
| 20 | 2008-04-18 21:10:19 | 1.84702599493292 | 20 | 1.6927111728445972 |
Now we can plot the graphs. In vastorbit we have multiple options to plot the graphs with different syntax of customization. For a complete list of all the graphs and their options check out the Chart Gallery.
Now let’s first try to plot this using Matplotlib:
import matplotlib.pyplot as plt
from matplotlib.pyplot import axhline
# Switch the plotting library to Matplotlib
vo.set_option("plotting_lib", "matplotlib")
fig = plt.figure()
ax = d_cap.plot(ts = "discharge_id", columns = ["Capacity", "smooth_capacity"])
ax.axhline(y = 1.4, label = "End-of-life criteria")
ax.set_title("Capacity degradation curve of the battery, its smoothed version and its end-of-life threshold")
ax.legend()
plt.show()
We can now try to plot it using Plotly. We can conveniently switch between the plotting libraries using:
# Switch the plotting library to Plotly
vo.set_option("plotting_lib", "plotly")
import plotly.graph_objects as go
plot = d_cap.plot(ts = "discharge_id", columns = ["Capacity", "smooth_capacity"], title = "Capacity degradation curve of the battery, its smoothed version and its end-of-life threshold")
# Add horizontal line
plot.add_hline(y = 1.4, line_width = 3, line_dash = "dash", line_color = "green")
# Add legend for the horizontal line
plot.add_trace(go.Scatter(x = [None], y = [None], mode = "lines", line = dict(color="green", width=3, dash="dash"), name = "End-of-life criteria"))
The sudden increases in battery capacity come from the self-charging property of Li-ion batteries. The smoothed graph makes the downward trend in the battery’s capacity very clear.
An important observation here is that the battery meets the EOL criteria around the 125th cycle.
Goal and Problem Modeling¶
Understanding battery health is important, but at the time of writing, there’s no direct way to measure it. In our case, we’ll create a degredation model to find the relationship between a battery’s overall health and the other properties in the dataset, which includes charge and discharge cycle duration, average voltage and current, etc.
One possible definition of the battery’s overall health (“state of health” or “SoH”) is the following:
Let \(Cap_{rate}\) be the rated capacity of the battery when it’s new (2Ah in our case), and \(Cap_{actual}\) be the actual capacity of the battery at a specific time. The state of health of the battery is defined as:
Data preparation¶
Outliet detection¶
Let’s start by finding and removing the global outliers from our dataset.
battery5.outliers(
columns = [
"Voltage_measured",
"Current_measured",
"Temperature_measured","Capacity",
],
name = "global_outlier",
threshold = 4.0,
)
battery5.filter("global_outlier = 0").drop("global_outlier")
Feature engineering¶
Since measurements like voltage and temperature tend to differ within the different cycles, we’ll create some features that can describe those cycles.
sample_cycle = battery5[battery5["Capacity"] == 1.83514614292266]
sample_cycle["Voltage_measured"].plot(ts = "Time")
sample_cycle["Temperature_measured"].plot(ts = "Time")
We’ll define new features that describe the minimum and maximum temperature during one cycle; the minimal voltage; and the time needed to reach minimum voltage and maximum temperature.
# filter for discharge cycles
discharging_data = battery5[battery5["type"] == "discharge"]
# define new features
discharge_cycle_metrics = discharging_data.groupby(
columns = ["start_time"],
expr = [
"MIN(Temperature_measured) AS min_temp",
"MAX(Temperature_measured) AS max_temp",
"MIN(Voltage_measured) AS min_volt",
]
).join(
discharging_data,
how = "left",
on = {"min_volt": "voltage_measured"},
expr1 = ["*"],
expr2 = ["Time AS time_to_reach_minvolt"],
).join(
discharging_data,
how = "left",
on = {"max_temp": "temperature_measured"},
expr1 = ["*"],
expr2 = ["Time AS time_to_reach_maxtemp"],
)
# calculate values of SOH
discharging_data = discharging_data.groupby(["start_time", "Capacity"])
discharging_data["SOH"] = discharging_data["Capacity"] * 0.5
# define the final dataset and save it to db
final_df = discharge_cycle_metrics.join(
discharging_data,
on_interpolate = {"start_time": "start_time"},
how = "left",
expr1 = discharge_cycle_metrics.get_columns(),
expr2 = ["SOH AS SOH"],
)
# normalize the features
final_df.normalize(
method = "minmax",
columns = [
"min_temp",
"max_temp",
"min_volt",
"time_to_reach_minvolt",
"time_to_reach_maxtemp",
],
)
# save it to db (materialized as a table so downstream queries stay small)
final_df.to_db(name = "finaldata_battery_5", relation_type = "table")
Machine Learning¶
We can now build a regression model to estimate the battery’s
State of Health (SOH) from the engineered features.
LinearRegression
offers a good balance of accuracy and interpretability for this dataset.
from vastorbit.machine_learning.vast import LinearRegression
final_model = LinearRegression(name = "btr_lr1")
final_model.fit(
final_df,
X = [
"min_temp",
"max_temp",
"min_volt",
"time_to_reach_minvolt",
"time_to_reach_maxtemp",
],
y = "SOH",
)
We can evaluate the model with a regression report.
final_model.regression_report()
| value | |
|---|---|
| explained_variance | 0.9612009734097492 |
| max_error | 0.08195958721894148 |
| median_absolute_error | 0.007948597664127859 |
| mean_absolute_error | 0.012847025290883152 |
| mean_squared_error | 0.15074810178462253 |
| root_mean_squared_error | 0.01874546909239797 |
| r2 | 0.9611928045786778 |
| r2_adj | 0.9599724525214035 |
| aic | -299.21658904644966 |
| bic | -281.5682579751981 |
The predictive power of our model looks pretty good. Let’s use our model to predict the SoH of the battery. We can visualize our prediction with a plot against the true values.
# take the predicted values and then plot them along the true ones
result = final_model.predict(
final_df,
name = "SOH_estimates",
)
result.plot(
ts = "start_time",
columns = ["SOH", "SOH_estimates"],
)
Conclusion¶
We successfully defined a battery degradation model that can make accurate predictions about the health of a Li-ion battery. This model could be used to, for example, accurately send warnings to users when their batteries meet the EOL criteria.