# EV Charger Power

As discussed in [batterycapacity](https://docs.dimo.org/dimo-explorer/field-descriptions-in-detail/electric-vehicle-tesla-only/batterycapacity "mention"), most vehicles do not report `batteryCapacity` so the battery capacities for specific make, model, year, and trim for some specific Teslas are given in this file:

{% file src="<https://3212476574-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2Foh9c3w7RI0Bl5tVCxcyz%2Fuploads%2FNDy3hydxMmjCuqOYTIiK%2Fbattery_capacity_for_some_teslas.csv?alt=media&token=90afa3fe-40d1-461d-8157-fa5c27593f05>" %}
File containing manually researched battery capacity for Teslas&#x20;
{% endfile %}

The code to calculate full charging sessions in the [#code-appendix](#code-appendix "mention") can be used to generate information about charging sessions. For example, an example level three charging session is given here

{% code overflow="wrap" lineNumbers="true" %}

```python
bat_capacity = 75
counties = gpd.read_file('tl_2022_us_county.zip')
states = gpd.read_file('tl_2022_us_state.zip')
urban = gpd.read_file('tl_2022_us_uac10.zip')

out_full = calculate_charging_sessions(df, urban, states, counties, bat_capacity)
```

{% endcode %}

<figure><img src="https://3212476574-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2Foh9c3w7RI0Bl5tVCxcyz%2Fuploads%2Fejw9LnIpMwQTCN4OvylX%2Fexample_charging_session.png?alt=media&#x26;token=c6df959c-f8eb-470f-bfd0-f7b9a5a0749c" alt=""><figcaption><p>Example charging session</p></figcaption></figure>

The charging session data can be aggregated based on the locations as in the below code snippet. Note that the charging sessions are aggregated based on the first two digits of the latitude and longitude of the GPS locations at which the charge started.

Using two digits can mean that some charging locations are combined into a single coordinate pair, but using three digits sometimes means a single charger is split into multiple coordinate pairs. While three decimal places generally gives about 111 meter accuracy (at the equator), the error in the gps signal itself (i.e. gps drift) means the location itself is imperfect. Two decimal places generally gives about 1110 meter accuracy, which is fairly large, but typically public charging stations will not be this close together.

`session_df` contains the concatenated results of `calculate_charging_sessions` for multiple vehicles.

At the end `gb` contains:

1. `rounded_point_str`: the location for these charges
2. `kw_est_mean`: the average estimated rate of charge (in kilowatts) based on the change in state of charge and the battery capacity (in kilowatt-hours)
3. `kw_first_mean`: the average first `charger_power` observation for charging sessions at this location
4. `kw_last_mean`: the average last `charger_power` observation for charging sessions at this location
5. `subject_count`: the number of charges to occur at this location
6. `subject_nunique`: the number of unique vehicles that charged at this location&#x20;

<pre class="language-python" data-overflow="wrap" data-line-numbers><code class="lang-python">import geopandas as gpd

<strong>session_df = session_df.loc[(session_df['diff_start_end_location_m'] == 0) &#x26;(session_df['time_charging'] >  np.timedelta64(30, 's')) &#x26; (session_df['net_charge_added'] > 0)]
</strong>
session_df['latitude'] = session_df['geometry'].y
session_df['longitude'] = session_df['geometry'].x
session_df['rounded_latitude'] = round(session_df['geometry'].y,2)
session_df['rounded_longitude'] = round(session_df['geometry'].x,2)
session_df['rounded_point'] = gpd.GeoSeries.from_xy(session_df['rounded_longitude'],session_df['rounded_latitude'])
session_df['rounded_point_str'] = session_df['rounded_point'].astype(str)
session_df['Charger_Level'] = pd.cut(session_df['kw_est'], bins = [0,2.5,30,np.Inf], labels = ['Level_1','Level_2','Level_3'])  

gb = session_df.groupby('rounded_point_str').agg({'kw_est':np.mean, 'kw_first':np.mean,'kw_last':np.mean,'subject':['count','nunique']})
gb.columns = [i[0] + '_' + i[1] for i in gb.columns]
gb = gb.reset_index(drop = False)
</code></pre>

The following code will combine the list of `soc` values and `charger_power` values to plot the  `charger_power` versus `soc` for all observed values of `soc`

<pre class="language-python" data-overflow="wrap" data-line-numbers><code class="lang-python"><strong>import matplotlib.pyplot as plt
</strong>pt = 'POINT (-119.429 49.886)'
subjects = charging_sessions.loc[
        charging_sessions['rounded_start_pt_str'] == pt]['subject'].unique()

powers = []
socs = []

powers = []
socs_s = []

for subject in subjects:
    powers.append(charging_sessions.loc[(charging_sessions['rounded_start_pt_str'] == pt) &#x26; (charging_sessions['kw_record_count'] > 0) &#x26; (charging_sessions['subject'] == subject)].explode('charger_power_lst')['charger_power_lst'].values)
    socs_s.append(charging_sessions.loc[(charging_sessions['rounded_start_pt_str'] == pt) &#x26; (charging_sessions['kw_record_count'] > 0) &#x26; (charging_sessions['subject'] == subject)].explode('soc_lst')['soc_lst'].values)



plt.cla()
plt.clf()
plt.close()

fig, ax = plt.subplots(figsize=(8,6))

colors = ['blue','green','red']

subject_names = ['Vehicle A','Vehicle B','Vehicle C']

for socs, power, color, subject in zip(socs_s, powers, colors, subject_names):
    ax.plot(socs,power,f'*', color = color, label = subject)
    
ax.xaxis.set_major_formatter(mtick.PercentFormatter(1, decimals = 0))


plt.legend()
plt.xlabel('soc')
plt.ylabel('kw (observed)')

plt.title('Kilowatts versus State of Charge for a Particular Charger')

plt.savefig('kw_true_vs_soc_subject.png')
</code></pre>

And here is an example graph where it can be seen that the charger can delivery about 150 kilowatts, but the vehicle's battery management system will decrease the power as the `soc` approaches 100%.

<figure><img src="https://3212476574-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2Foh9c3w7RI0Bl5tVCxcyz%2Fuploads%2FPVILvfx68w6Vx9R1PC9z%2Fkw_true_vs_soc_subject.png?alt=media&#x26;token=db936c40-9038-411b-ada0-a2771564a46c" alt=""><figcaption></figcaption></figure>

Some aggregated charging session data can be found on this map: <https://felt.com/map/Charger-Power-Data-NCYlp5WxRVmW0NQ9CVrHO7A?loc=46.12,-73.74,3z&share=1> .&#x20;

Additionally, to be able to compare power delivered during different phases of charging sessions across many vehicles when `charger_power` is not available, charging sub-sessions can be calculated as well.&#x20;

{% code overflow="wrap" lineNumbers="true" %}

```python
import geopandas as gpd

subsession_df = subsession_df.loc[subsession_df['geometry'].notna()]

subsession_df['latitude'] = subsession_df['geometry'].y
subsession_df['longitude'] = subsession_df['geometry'].x
subsession_df['rounded_latitude'] = round(subsession_df['geometry'].y,2)
subsession_df['rounded_longitude'] = round(subsession_df['geometry'].x,2)
subsession_df['rounded_point'] = gpd.GeoSeries.from_xy(subsession_df['rounded_longitude'],subsession_df['rounded_latitude'])
subsession_df['rounded_point_str'] = subsession_df['rounded_point'].astype(str)
subsession_df = subsession_df.loc[subsession_df['kw_est'].notna()]
subsession_df['Charger_Level'] = pd.cut(subsession_df['kw_est'], bins = [0,2.5,30,np.Inf], labels = ['Level_1','Level_2','Level_3'])

gb = subsession_df.groupby(['rounded_point_str','soc_bin']).agg({'kw_est':np.mean,'kw_first':np.mean, 'kw_last':np.mean,'subject':['count','nunique']})
gb.columns = [i[0] + '_' + i[1] for i in gb.columns]
gb = gb.reset_index(drop = False)
  
```

{% endcode %}

### Code Appendix

<pre class="language-python" data-title="Full Charging Sessions" data-overflow="wrap" data-line-numbers><code class="lang-python">import geopandas as gpd
import numpy as np
import pandas as pd

counties = gpd.read_file('tl_2022_us_county.zip')
states = gpd.read_file('tl_2022_us_state.zip')
urban = gpd.read_file('tl_2022_us_uac10.zip')
tz = gpd.read_file('ne_10m_time_zones.geojson')

def calculate_charging_sessions(df, urban, states, counties, bat_capacity):
    # This function calculates charging sessions and joins against 
    # Input:
    #     gdf: a GeoDataFrame
    #     urban: a GeoDataFrame containing urban areas from https://www2.census.gov/geo/tiger/TIGER2022/UAC/
    #     states: a GeoDataFrame containing state areas from https://www2.census.gov/geo/tiger/TIGER2022/STATE/
    #     counties: a GeoDataFrame containing county areas from https://www2.census.gov/geo/tiger/TIGER2022/COUNTY/
    #     bat_capacity: battery capacity in kilowatts
    # Output:
    #     aggregated charging sessions dataframe. 

<strong>    df = df.loc[df['latitude'].notna()]
</strong>    df = df.loc[df['charging'].notna()]
    df = df.loc[df['soc'].notna()]

    gdf = gpd.GeoDataFrame(df)
    gdf['geometry'] = gpd.GeoSeries.from_xy(x=gdf['longitude'],y=gdf['latitude'])
    gdf.crs = "EPSG:4326" 
    gdf['charging'] = gdf['charging'].fillna(False)

    gdf['timestamp'] = pd.to_datetime(gdf['timestamp'])
    gdf = gdf.sjoin(tz[['geometry','tz_name1st']])
    gdf['timestamp'] = gdf.apply(lambda row: row['timestamp'].tz_convert(row['tz_name1st']),  axis=1)
    
    gdf = gdf.drop(columns = 'index_right')

    joined_gdf = gdf.sjoin(urban[['NAME10','geometry','UATYP10']].to_crs(gdf.crs), how = 'left')
    joined_gdf['UATYP10'] = joined_gdf.UATYP10.fillna('R')
    joined_gdf2 = gdf.sjoin(states[['NAME','geometry']].to_crs(gdf.crs), how = 'left') 
    joined_gdf = joined_gdf.merge(joined_gdf2[['NAME']], left_index = True, right_index = True).rename(columns = {'NAME':'STATE'})
    joined_gdf['STATE'] = joined_gdf.STATE.fillna('NON_US')
    counties = counties.rename(columns = {'NAME':'COUNTY'})
    joined_gdf2 = gdf.sjoin(counties[['COUNTY','geometry']].to_crs(gdf.crs), how = 'left') 
    joined_gdf = joined_gdf.merge(joined_gdf2[['COUNTY']],
                          left_index = True, right_index = True)

    gdf = joined_gdf


    gdf.loc[:,'lat_rounded_2'] = gdf.loc[:,'latitude'].round(decimals=2)
    gdf.loc[:,'lng_rounded_2'] = gdf.loc[:,'longitude'].round(decimals=2)

    gdf.loc[:,'lat_rounded_3'] = gdf.loc[:,'latitude'].round(decimals=3)
    gdf.loc[:,'lng_rounded_3'] = gdf.loc[:,'longitude'].round(decimals=3)

    gdf['rounded_point'] = gpd.GeoSeries.from_xy(gdf['lng_rounded_3'],gdf['lat_rounded_3'])
    gdf = gdf.sort_values('timestamp').reset_index(drop = True)
    gdf['timestamp_diff'] = gdf['timestamp'].diff()
    
    gdf['distance_added'] = gdf['odometer'].diff()
    gdf['delta_soc_per_minute'] = gdf['soc'].diff()/(gdf['timestamp'].diff()/np.timedelta64(1, 'm'))

    gdf['dist_to_prev_pt'] = gdf['geometry'].to_crs('EPSG:6340').distance(gdf['geometry'].shift(1).to_crs('EPSG:6340'))
    gdf['dist_to_prev_pt_shift'] = gdf['dist_to_prev_pt'].shift(1)

    gdf['charging_event_id'] = gdf['charging'].diff().cumsum().fillna(0)

    gb = gdf.loc[gdf['charging']].groupby('charging_event_id')
    charging_df = gpd.GeoDataFrame(gb['timestamp'].max() - gb['timestamp'].min())
    charging_df.columns = ['time_charging']
    charging_df['start_location'] = gb['geometry'].first()
    charging_df['end_location'] = gb['geometry'].nth(-1)
    charging_df['start_location'].crs = 'EPSG:4326'
    charging_df['end_location'].crs = 'EPSG:4326'


    charging_df['first_time'] = gb['timestamp'].first()
    charging_df['last_time'] = gb['timestamp'].nth(-1)


    charging_df['diff_start_end_location_m'] = charging_df['end_location'].to_crs('EPSG:6340').distance(charging_df['start_location'].to_crs('EPSG:6340'))
    charging_df['start_location'] = gb['geometry'].first()
    charging_df['end_location'] = gb['geometry'].nth(-1)

    charging_df['start_soc'] = gb['soc'].first()
    charging_df['end_soc'] = gb['soc'].nth(-1)
    charging_df['net_charge_added'] = charging_df['end_soc']- charging_df['start_soc']
    charging_df['max_charge_added'] = gb['soc'].max() -gb['soc'].min()

    charging_df['max_charging_rate'] = gb['delta_soc_per_minute'].max()
    charging_df['min_charging_rate'] = gb['delta_soc_per_minute'].min()
    charging_df['mean_charging_rate'] = gb['delta_soc_per_minute'].mean()
    charging_df['overall_mean_charging_rate'] = (charging_df['end_soc'] - charging_df['start_soc'])/(charging_df['time_charging']/np.timedelta64(1, 'm'))

    charging_df['start_range'] = gb['range'].first()
    charging_df['end_range'] = gb['range'].nth(-1)
    charging_df['net_range_added'] = charging_df['end_range']- charging_df['start_range']
    charging_df['max_range_added'] = gb['range'].max() -gb['range'].min()

    charging_df['pop_density'] = gb['UATYP10'].first()
    charging_df['state'] = gb['STATE'].first()
    charging_df['county'] = gb['COUNTY'].first()

    charging_df['bat_capacity'] = bat_capacity
    charging_df['duration_hours'] = charging_df['time_charging']/np.timedelta64(1, 'h')
    charging_df['kwh_est'] = (charging_df['end_soc']- charging_df['start_soc'])*bat_capacity
    charging_df['kw_est'] = charging_df['kwh_est']/charging_df['duration_hours']


    try:
        charging_df['kw_first'] = gb['charger_power'].first()
        charging_df['kw_last'] = gb['charger_power'].nth(-1)
        charging_df['kw_max'] = gb['charger_power'].max()
        charging_df['kw_mean'] = gb['charger_power'].mean()
        charging_df['kw_median'] = gb['charger_power'].median()
        charging_df['kw_record_count'] = gb['charger_power'].count()
        charging_df['unique_charger_power'] = gb['charger_power'].unique()


        temp = gdf[['charging_event_id']].merge(gb['charger_power'].sample(frac = 1),left_index = True, right_index = True)

        temp2 = gdf[['charging_event_id']].merge(gb['soc'].sample(frac = 1),left_index = True, right_index = True)

        lst = []
        lst2 = []

        for charging_event_id in list(gb['charger_power'].groups.keys()):
            lst.append(temp.loc[temp['charging_event_id'] == charging_event_id]['charger_power'].tolist())
            
            lst2.append(temp2.loc[temp2['charging_event_id'] == charging_event_id]['soc'].tolist())

        charging_df['charger_power_lst'] = lst
        charging_df['soc_lst'] = lst2
        
        lst = []
        lst2 = []
        for idx, row in charging_df.iterrows():
            temp = gdf.loc[(gdf['charging_event_id'] == idx)]
            temp = temp.loc[np.abs(pd.to_datetime(temp['timestamp']) - row['first_time']) &#x3C; np.timedelta64(5,'m')]
            lst.append(temp['charger_power'].mean())
            
            temp = gdf.loc[(gdf['charging_event_id'] == idx)]
            temp = temp.loc[np.abs(pd.to_datetime(temp['timestamp']) - row['last_time']) &#x3C; np.timedelta64(5,'m')]
            lst2.append(temp['charger_power'].mean())
            
        
        charging_df['kw_first_five_min'] = lst
        charging_df['kw_last_five_min'] = lst2

    except KeyError:
        charging_df['kw_first'] = np.nan
        charging_df['kw_last'] = np.nan
        charging_df['kw_max'] = np.nan
        charging_df['kw_mean'] = np.nan
        charging_df['kw_median'] = np.nan
        charging_df['kw_record_count'] = np.nan
        charging_df['unique_charger_power'] = np.nan
        charging_df['charger_power_lst'] = np.nan
        charging_df['soc_lst'] = np.nan

    return charging_df
</code></pre>

{% code title="Charging Sub-sessions" lineNumbers="true" %}

```python
def calculate_charging_subsessions(df, urban, states, counties, bat_capacity):
    df = df.loc[df['latitude'].notna()]
    df = df.loc[df['charging'].notna()]
    df = df.loc[df['soc'].notna()]

    gdf = gpd.GeoDataFrame(df)
    gdf['geometry'] = gpd.GeoSeries.from_xy(x=gdf['longitude'],y=gdf['latitude'])
    gdf.crs = "EPSG:4326" 
    gdf['charging'] = gdf['charging'].fillna(False)

    # joined_gdf = gdf.sjoin(urban[['NAME10','geometry','UATYP10']].to_crs(gdf.crs), how = 'left')
    # joined_gdf['UATYP10'] = joined_gdf.UATYP10.fillna('R')
    # joined_gdf2 = gdf.sjoin(states[['NAME','geometry']].to_crs(gdf.crs), how = 'left') 
    # joined_gdf = joined_gdf.merge(joined_gdf2[['NAME']], left_index = True, right_index = True).rename(columns = {'NAME':'STATE'})
    # joined_gdf['STATE'] = joined_gdf.STATE.fillna('NON_US')
    # counties = counties.rename(columns = {'NAME':'COUNTY'})
    # joined_gdf2 = gdf.sjoin(counties[['COUNTY','geometry']].to_crs(gdf.crs), how = 'left') 
    # joined_gdf = joined_gdf.merge(joined_gdf2[['COUNTY']],
    #                     left_index = True, right_index = True)
    # gdf = joined_gdf

    gdf.loc[:,'lat_rounded_2'] = gdf.loc[:,'latitude'].round(decimals=2)
    gdf.loc[:,'lng_rounded_2'] = gdf.loc[:,'longitude'].round(decimals=2)

    gdf.loc[:,'lat_rounded_3'] = gdf.loc[:,'latitude'].round(decimals=3)
    gdf.loc[:,'lng_rounded_3'] = gdf.loc[:,'longitude'].round(decimals=3)

    gdf['rounded_point'] = gpd.GeoSeries.from_xy(gdf['lng_rounded_2'],gdf['lat_rounded_2'])
    #gdf['timestamp_str'] = gdf['timestamp'].astype(str)
    gdf['timestamp'] = pd.to_datetime(gdf['timestamp'])

    gdf = gdf.sjoin(tz[['geometry','tz_name1st']])
    gdf['timestamp'] = \
        gdf.apply(lambda row: row['timestamp'].tz_convert(row['tz_name1st']), \
                        axis=1)

    gdf = gdf.sort_values('timestamp').reset_index(drop = True)
    gdf['timestamp_diff'] = gdf['timestamp'].diff()

    gdf['distance_added'] = gdf['odometer'].diff()


    gdf['dist_to_prev_pt'] = gdf['geometry'].to_crs('EPSG:6340').distance(gdf['geometry'].shift(1).to_crs('EPSG:6340'))
    gdf['dist_to_prev_pt_shift'] = gdf['dist_to_prev_pt'].shift(1)

    gdf['soc_bins'] = pd.cut(gdf['soc'], bins = np.linspace(0,1,5), include_lowest = True)

    gdf['charging_event_id_total'] = gdf['charging'].diff().cumsum().fillna(0)

    gdf['charging_event_id'] = np.nan

    gdf['charging_and_soc_bin'] = gdf['charging'].astype(str) + '_' + gdf['soc_bins'].astype(str)


    idxs = gdf.loc[gdf['charging']].index

    gdf['charging_event_and_soc_bin_eq_next'] = ((gdf.loc[idxs,'soc_bins'] != gdf.loc[idxs,'soc_bins'].shift(1))\
            | (gdf.loc[idxs,'charging_event_id_total'] != gdf.loc[idxs,'charging_event_id_total'].shift(1)))

    gdf.loc[idxs, 'charging_event_id'] = \
            ((gdf.loc[idxs,'soc_bins'] != gdf.loc[idxs,'soc_bins'].shift(1))\
           | (gdf.loc[idxs,'charging_event_id_total'] != gdf.loc[idxs,'charging_event_id_total'].shift(1)))\
            .cumsum().fillna(0)


    df_lst = []

    for g in [i for i in gdf['charging_event_id_total'].unique()]:
        
        #print(g)
        if int(100*gdf.loc[gdf['charging_event_id_total'] == g].iloc[0]['soc']) % 5 == 0:
            gdf.loc[gdf['charging_event_id_total'] == g].iloc[0]['soc'] -= 0.01


        temp_gdf = gdf.loc[gdf['charging_event_id_total'] == g]
        if temp_gdf.shape[0] <= 1 or (temp_gdf['charging'].mean() == 0):
            continue
        while (temp_gdf['soc'].diff(-1) > 0).sum() >= 1:
            temp_gdf = temp_gdf.loc[temp_gdf['soc'].diff(-1) <= 0]

        if temp_gdf.shape[0] <= 1:
            continue
        range_df = pd.DataFrame(columns = ['val'], data= np.arange(temp_gdf.soc.min(),temp_gdf.soc.max() + 0.01, 0.01))
        if set(temp_gdf['soc'].values) == set([round(i,2) for i in range_df['val'].values]):
            df_lst.append(temp_gdf)
        else:
            #new_df = pd.merge_asof(temp_gdf, range_df , right_on = 'val', left_on = 'soc').drop(columns = ['soc'])
            new_df = pd.merge_asof(range_df, temp_gdf , right_on = 'soc', left_on = 'val').drop(columns = ['soc'])
            new_df = new_df.rename(columns = {'val':'soc'})

            df_lst.append(new_df)
    gdf2 = pd.concat(df_lst).reset_index(drop = True)

    gdf = gdf2.copy(deep = True)

    gdf = gdf.loc[gdf['soc'].notna()].reset_index(drop = True)

    label_id = 'charging_event_id' #'charging_event_id' #'charging_event_id_total'
    gb = gdf.groupby(label_id)['charging'].count()
    charging_event_ids = gb.loc[gb > 1].index

    gb = gdf.loc[gdf['charging_event_id'].isin(charging_event_ids)].groupby(label_id)['charging_event_id_total'].first()
    charging_event_ids_total = gb.values

    time_max = gdf.loc[gdf[label_id].isin(charging_event_ids)].groupby(label_id)['timestamp'].max()
    time_min = gdf.loc[gdf[label_id].isin(charging_event_ids)].groupby(label_id)['timestamp'].min()

    duration_hours = (time_max - time_min)/np.timedelta64(1,'h')

    soc_first = gdf.loc[gdf[label_id].isin(charging_event_ids)].groupby(label_id)['soc'].first()
    soc_last = gdf.loc[gdf[label_id].isin(charging_event_ids)].groupby(label_id)['soc'].last()

    gb = gdf[['charging_event_id','charging_event_id_total']]\
    .merge(soc_first, left_on = 'charging_event_id', right_index = True)#.set_index('charging_event_id')

    soc_bins = pd.merge(pd.DataFrame(index = charging_event_ids), gdf, left_index = True, right_on = label_id)\
        .groupby(label_id)['soc_bins'].first()
    
    gb = gdf.loc[gdf[label_id].isin(charging_event_ids)].groupby(label_id)

    charging_w_bins = pd.DataFrame()
    charging_w_bins['charging_event_id'] = charging_event_ids
    if label_id == 'charging_event_id':
        charging_w_bins['charging_event_id_total'] = charging_event_ids_total
    charging_w_bins['bat_capacity'] = bat_capacity
    charging_w_bins['soc_bin'] = soc_bins.values
    charging_w_bins['soc_bin'] = charging_w_bins['soc_bin'].astype(str)
    charging_w_bins['soc_left'] = charging_w_bins['soc_bin'].map(lambda x: float(x.split(',')[0][1:]))
    charging_w_bins['soc_first']= soc_first.values
    charging_w_bins['soc_last'] = soc_last.values
    charging_w_bins['time_min'] = time_min.values
    charging_w_bins['time_max'] = time_max.values
    charging_w_bins['duration_hours'] = duration_hours.values
    charging_w_bins['first_rounded_pt'] = gb['rounded_point'].first().values
    charging_w_bins['last_rounded_pt'] = gb['rounded_point'].last().values

    charging_w_bins['first_pt'] = gb['geometry'].first().values
    charging_w_bins['last_pt'] = gb['geometry'].last().values

    charging_w_bins['start_range'] = gb['range'].first().values
    charging_w_bins['end_range'] = gb['range'].last().values


    if label_id == 'charging_event_id':
        idx = charging_w_bins.loc[~charging_w_bins['charging_event_id']\
            .isin(charging_w_bins.groupby('charging_event_id_total')['charging_event_id'].first())].index
        charging_w_bins.loc[idx,'soc_first'] = charging_w_bins.loc[idx,'soc_first'] - 0.01

    charging_w_bins['kwh_est'] = (charging_w_bins['soc_last']- charging_w_bins['soc_first'])*bat_capacity
    charging_w_bins['kw_est'] = charging_w_bins['kwh_est']/charging_w_bins['duration_hours']

    try:
        charging_w_bins['kw_first'] = gb['charger_power'].first().values
        charging_w_bins['kw_last'] = gb['charger_power'].nth(-1).values
        charging_w_bins['kw_mean'] = gb['charger_power'].mean().values
        charging_w_bins['kw_median'] = gb['charger_power'].median().values
        charging_w_bins['kw_record_count'] = gb['charger_power'].count().values
    except KeyError:
        charging_w_bins['kw_first'] = np.nan
        charging_w_bins['kw_last'] = np.nan
        charging_w_bins['kw_mean'] = np.nan
        charging_w_bins['kw_median'] = np.nan
        charging_w_bins['kw_record_count'] = np.nan
    
    return charging_w_bins
```

{% endcode %}
