-
타이어 마모 인덱스application 2026. 4. 10. 13:49
타이어 마모 인덱스¶
- 타이어는 언제 많이 마모될까?
- 휠 슬립율이 클 때
- 같은 슬립율이라도 아스팔트, 코블스톤, 흙길, 자갈길 등 노면 종류에 따라 다를 것이다.
- 타이어에 걸리는 수직 하중이 클 때
- 같은 슬립율이라도 수직 하중이 클 때 더 마모가 많이 될 것 같다.
- 코너링할 때
- 사이드 슬립이 크면 많이 닿겠지.
- 그 외에 많은 다른 요인이 있을 것이다. 타이어 공기압, 온도, 비/눈, ...
- Tlog100x로 측정한 CAN 데이터와 GPS 데이터 처리하는 방법을 데모하는 목적이니까 단순하게 계산해본다.
- 휠 슬립율이 클 때
- 휠 슬립율을 어떻게 구할까?
- 휠 슬립율은 아래와 같이 정의한다.
r_slip_xx = (ws_xx - vs) / vs * 100 ws: wheel_speed vs: vehicle_speed xx: fl, fr, rl, rr fl: front left fr: front right rl: rear left rr: rear right - vs는 어떻게 구할까?
- 여러 가지 옵션이 있다.
- A: GPS 좌표를 이용해서 구한다.
- B: CAN 신호들에서 구한다.
- A: GPS 좌표를 이용해서 구하기
- 두 지점 (위도, 경도) 좌표들로 부터 거리를 구한다. 거리와 이동 시간을 이용해서 속도를 구한다.
- 데모의 취지에 맞다.
- GPS 신호가 갖고 있는 오차 때문에 충분히 정확하지 못할 것이다.
- B: CAN 신호들에서 구하기
- 측정한 데이터에 ABS 작동 같은 깊은 휠 슬립이 발생하는 경우가 없다. 아래와 같이 한다.
- 감속 중에는 가장 빠른 바퀴 속도를 vs로 하자.
- 가속 중에는 가장 느린 바퀴 속도를 vs로 하자.
- 선회 중일 때는 안쪽 바퀴와 바깥쪽 바퀴 속도 차이가 있으니까 ... 네 바퀴 속도 대신 앞축의 평균과 뒤축의 평균을 사용하여 가감속 상태를 고려하여 vs를 계산하자.
- 가감속이 없을 때는 네 바퀴의 평균을 vs로 하자.
- 가감속을 어떻게 판단할 것인가?
- 얼핏 3가지 방법이 생각난다. A, B, C라고 하자.
- 가감속 판단 A
- Engine Speed와 Transmission Input Shaft Speed (TISS)를 비교한다.
- engine_speed > tiss : 가속
- engine_speed < tiss : 감속
- 단순히 위와 같이 정의하면 가감속이 너무 자주 바뀌는 소위 busy state change 문제가 발생할 것이다. 이를 방지하기 위해서 히스테리시스가 필요하다.
- 토크 컨버터가 잠겨 (lock) 직결될 수 있다. 이것도 고려해야 하는가? <-- 이 신호가 CAN에 없다.
- 가감속 판단 B
- 브레이크 마스터 실린더 압력 > 5 bar 이상일 때를 제동으로 본다.
- 가속 페달 위치 > 2% 이상일 때를 구동으로 본다.
- 브레이크도 밟지 않고 가속 페달도 밟지 않은 상태에서 엔진 브레이크로 감속하는 경우는?
- 엔진과 연결된 바퀴들이 그렇지 않은 바퀴들 보다 더 마모 되지 않을까?
- 가감속 판단 C
- 엔진이 생산하는 토크와 엔진의 마찰 토크를 비교한다.
- 엔진이 생산하는 토크: TQI
- 엔진의 마찰 토크: TQFR <-- 이 신호가 CAN에 없다.
- A 방법으로 해보자.
- 여러 가지 옵션이 있다.
사전 준비¶
- 주행하면서 Tlog1004로 CAN 신호와 GPS 신호를 함께 저장한다.
- Tlog1004에 저장된 데이터를 blf 파일로 다운로드 받는다.
- TSMaster의 Log Converter 기능을 이용하여 blf 파일을 GPS 신호를 포함한 asc 파일로 변환한다.
- 전에 개발한 blf_asc_dbc_to_mdf_csv_mp_ctb.py를 이용하여 asc를 mdf로 변환한다. 변환할 때, 측정 주기를 100msec로 한다.
In [1]:# modules from pathlib import Path import asammdf import pandas as pd import plotly.express as px import plotly.graph_objects as go from plotly.subplots import make_subplots from scipy.signal import medfilt, butter, filtfilt import numpy as npmdf 파일 읽기¶
In [2]:dir_current = Path.cwd() path_mdf = dir_current / r'venue_2026-03-11_08-11-53_resampled_100ms.mf4' mdf = asammdf.MDF(path_mdf)In [3]:# channels of interest coi = [ # 엔진 정보 'EMS11__N', # engine speed 'EMS12__PV_AV_CAN', # accelerator pedal position 'EMS12__TQ_STND', # engine torque standard 'EMS16__TQI', # engine torque 'EMS16__TQI_MIN', # engine torque minimum 'EMS16__TQI_MAX', # engine torque maximum # 기어 정보 'TCU13__CF_Tcu_TarGr', # gear (기어단) # 'TCU16__TCU_NCC_RPM', # Transmission Input Shaft Speed? 항상 0이어서 제외 'TCU16__CF_Tcu_NCC_Stat', # torque converter state으로 추정됨. 항상 일정해서 제외 'TCU15__TCU_RPM_Cluster', # Transmission Input Shaft Speed? 'TCU14__CF_Tcu_DrivingModeReq', # 항상 일정해서 제외 'TCU14__CF_Tcu_DrivingModeDisp', # 항상 일정해서 제외 # 마스터 실린더 압력 'ESP12__CYL_PRES', # brake pressure # 모션 유닛 'ESP12__LONG_ACCEL', # longitudinal acceleration 'ESP12__LAT_ACCEL', # lateral acceleration 'ESP12__YAW_RATE', # yaw rate # 바퀴 속도 'WHL_SPD11__WHL_SPD_FL', # wheel speed front left 'WHL_SPD11__WHL_SPD_FR', # wheel speed front right 'WHL_SPD11__WHL_SPD_RL', # wheel speed rear left 'WHL_SPD11__WHL_SPD_RR', # wheel speed rear right # GPS 정보 'longitude', # longitude (경도) 'latitude', # latitude (위도) 'altitude' # altitude (고도) ] # df로 변환한다. # 이렇게 쉽다니. blf_asc_dbc_to_mdf_csv_mp_ctb.py의 resampling 부분 코드를 수정해야겠다. df = mdf.to_dataframe(channels=coi) # 컬럼 이름을 변경한다. df.rename( columns={ 'timestamps': 'ts', # timestamp. 인덱스. 인덱스라 rename이 안 된다. 'EMS11__N': 'engine_speed', # engine speed 'EMS12__PV_AV_CAN': 'aps', # accelerator pedal position 'EMS12__TQ_STND': 'engine_torque_stnd', # engine torque standard 'EMS16__TQI': 'engine_torque', # engine torque 'EMS16__TQI_MIN': 'engine_torque_min', # engine torque minimum 'EMS16__TQI_MAX': 'engine_torque_max', # engine torque maximum 'TCU13__CF_Tcu_TarGr': 'gear', # gear (기어단) 'TCU16__TCU_NCC_RPM': 'tiss', # Transmission Input Shaft Speed? 항상 0이어서 제외 'TCU16__CF_Tcu_NCC_Stat': 'tc_stat', # torque converter state으로 추정됨. 항상 일정해서 제외 'TCU15__TCU_RPM_Cluster': 'tiss', # Transmission Input Shaft Speed 'TCU14__CF_Tcu_DrivingModeReq': 'driving_mode_req', # 항상 일정해서 제외 'TCU14__CF_Tcu_DrivingModeDisp': 'driving_mode_disp', # 항상 일정해서 제외 'ESP12__CYL_PRES': 'brake_pressure', # brake pressure 'ESP12__LONG_ACCEL': 'long_accel', # longitudinal acceleration 'ESP12__LAT_ACCEL': 'lat_accel', # lateral acceleration 'ESP12__YAW_RATE': 'yaw_rate', # yaw rate 'WHL_SPD11__WHL_SPD_FL': 'ws_fl', # wheel speed front left 'WHL_SPD11__WHL_SPD_FR': 'ws_fr', # wheel speed front right 'WHL_SPD11__WHL_SPD_RL': 'ws_rl', # wheel speed rear left 'WHL_SPD11__WHL_SPD_RR': 'ws_rr', # wheel speed rear right }, inplace=True )어떤 신호가 어떤 신호인지 그래프로 확인한다.¶
변속기 신호 확인¶
In [ ]:# gear, tc_stat, tiss, driving_mode_req, driving_mode_disp, aps, ws_fl, brake_pressure를 # 8 x 1 subplot으로 그려본다. fig = make_subplots(rows=8, cols=1, shared_xaxes=True, vertical_spacing=0.02) fig.add_trace(go.Scatter(x=df.index, y=df['ws_fl'], mode='lines', name='ws_fl'), row=1, col=1) fig.add_trace(go.Scatter(x=df.index, y=df['brake_pressure'], mode='lines', name='brake_pressure'), row=2, col=1) fig.add_trace(go.Scatter(x=df.index, y=df['aps'], mode='lines', name='aps'), row=3, col=1) fig.add_trace(go.Scatter(x=df.index, y=df['gear'], mode='lines', name='gear'), row=4, col=1) fig.add_trace(go.Scatter(x=df.index, y=df['tiss'], mode='lines', name='tiss'), row=5, col=1) fig.add_trace(go.Scatter(x=df.index, y=df['tc_stat'], mode='lines', name='tc_stat'), row=6, col=1) fig.add_trace(go.Scatter(x=df.index, y=df['driving_mode_req'], mode='lines', name='driving_mode_req'), row=7, col=1) fig.add_trace(go.Scatter(x=df.index, y=df['driving_mode_disp'], mode='lines', name='driving_mode_disp'), row=8, col=1) fig.update_layout(width=1000, height=1000, title_text="Transmission Signals Over Time") fig.show()발견 - 변속기 신호¶
- tc_stat, driving_mode_req, driving_mode_disp'는 값이 항상 일정하다. 유용하지 않다.
엔진 신호 확인¶
In [ ]:# 'engine_speed', 'aps', 'engine_torque', 'engine_torque_min', 'engine_torque_max' # 신호들을 5 x 1 subplot으로 그려본다. fig = make_subplots(rows=3, cols=1, shared_xaxes=True, vertical_spacing=0.02) fig.add_trace(go.Scatter(x=df.index, y=df['engine_speed'], mode='lines', name='engine_speed'), row=1, col=1) fig.add_trace(go.Scatter(x=df.index, y=df['aps'], mode='lines', name='aps'), row=2, col=1) fig.add_trace(go.Scatter(x=df.index, y=df['engine_torque_stnd'], mode='lines', name='engine_torque_stnd'), row=3, col=1) fig.add_trace(go.Scatter(x=df.index, y=df['engine_torque'], mode='lines', name='engine_torque'), row=3, col=1) fig.add_trace(go.Scatter(x=df.index, y=df['engine_torque_min'], mode='lines', name='engine_torque_min'), row=3, col=1) fig.add_trace(go.Scatter(x=df.index, y=df['engine_torque_max'], mode='lines', name='engine_torque_max'), row=3, col=1) fig.update_layout(width=1000, height=600, title_text="Engine Signals Over Time") fig.show()발견 - 엔진 신호¶
- 위 엔진 토크 신호들로는 가감속을 판정할 수 없다.
가감속 상태 계산¶
- 토크 컨버터 슬립 (torque converter slip)으로 가감속 상태를 판정한다.
In [6]:df['slip_tc'] = df['engine_speed'] - df['tiss']In [ ]:# 2 x 1 subplot으로 engine_speed, tiss, slip_tc를 그려본다. # 슬라이더를 추가하여 시간 범위를 조절할 수 있도록 한다. fig = make_subplots(rows=2, cols=1, shared_xaxes=True, vertical_spacing=0.02) fig.add_trace(go.Scatter(x=df.index, y=df['engine_speed'], mode='lines', line=dict(dash='dash'), name='engine_speed'), row=1, col=1) fig.add_trace(go.Scatter(x=df.index, y=df['tiss'], mode='lines', line=dict(dash='dash'), name='tiss'), row=1, col=1) fig.add_trace(go.Scatter(x=df.index, y=df['slip_tc'], mode='lines', name='slip_tc'), row=2, col=1) fig.update_layout(width=1000, height=500, title_text="Engine Speed, TISS, and Torque Converter Slip Over Time") fig.update_xaxes(rangeslider_visible=False, row=1, col=1) fig.update_xaxes(rangeslider_visible=True, row=2, col=1) fig.show()In [8]:df['slip_tc_sign_change'] = np.sign(df['slip_tc']).diff().fillna(0).abs() # 전체 데이터 수 total_data_points = len(df) print(f'{total_data_points = :,}') # slip_tc의 부호는 얼마나 자주 바뀌는가? num_sign_changes = df['slip_tc_sign_change'].sum() print(f'{num_sign_changes = :,.0f}') # slip_tc 부호가 바뀐 비율 sign_change_rate = num_sign_changes / total_data_points print(f'{sign_change_rate = :.1%}') print() df["slip_tc"].describe()total_data_points = 28,793 num_sign_changes = 19,420 sign_change_rate = 67.4%Out[8]:count 28793.000000 mean 0.003647 std 2.511806 min -42.500000 25% -0.500000 50% 0.000000 75% 0.500000 max 38.000000 Name: slip_tc, dtype: float64발견 - 가감속 상태¶
- 엔진 속도 (engine_speed)와 변속기 입력축 속도 (tiss: transmission input shaft speed)의 차이인 토크 컨버터 슬립(slip_tc: torque converter slip)이 두 신호에 비해 작아서 그래프에서 두 신호가 겹쳐보인다.
- 토크 컨버터 슬립의 평균은 거의 0 rpm 이다. 표준 편차는 2.5 rpm이다.
- 토크 컨버터 슬립의 부호가 너무 자주 (전체 데이터의 67%) 바뀐다. 즉, 가감속 상태 변화가 너무 심하다. 필터가 필요하다.
토크 컨버터 슬립 필터하기¶
- ai의 추천에 따라 median 필터와 butter 필터를 적용한다.
- 나는 위 두 필터가 어떤 원리인지 모른다. :-)
In [9]:# 샘플링 주파수 추정 (숫자 index/시간 index 모두 지원) didx = df.index.to_series().diff().dropna() if pd.api.types.is_timedelta64_dtype(didx): dt = didx.dt.total_seconds().median() else: dt = float(didx.median()) fs = 1.0 / dt # 1) 스파이크 제거 x = medfilt(df['slip_tc'].to_numpy(), kernel_size=7) # 2) 저역통과 필터 (zero-phase) b, a = butter(N=4, Wn=1.0 / (fs / 2), btype='low') df['slip_tc_filt'] = filtfilt(b, a, x)In [ ]:# slip_tc와 slip_tc_filt를 그래프로 비교한다. px.line( df, x=df.index, y=['slip_tc', 'slip_tc_filt'], title='Torque Converter Slip (Raw vs Filtered)', labels={'value': 'RPM', 'ts': 'Time'}, width=1000, height=600 )In [11]:df['slip_tc_filt_sign_change'] = np.sign(df['slip_tc_filt']).diff().fillna(0).abs() # 전체 데이터 수 total_data_points = len(df) print(f'{total_data_points = :,.0f}') # slip_tc_filt의 부호는 얼마나 자주 바뀌는가? num_sign_changes = df['slip_tc_filt_sign_change'].sum() print(f'{num_sign_changes = :,.0f}') # slip_tc_filt 부호가 바뀐 비율 sign_change_rate = num_sign_changes / total_data_points print(f'{sign_change_rate = :.1%}') print() df["slip_tc_filt"].describe()total_data_points = 28,793 num_sign_changes = 7,550 sign_change_rate = 26.2%Out[11]:count 2.879300e+04 mean 8.022783e-03 std 1.097424e+00 min -1.398160e+01 25% -8.988829e-02 50% -3.938577e-72 75% 4.936604e-02 max 1.242765e+01 Name: slip_tc_filt, dtype: float64발견 - slip_tc_filt¶
- 토크 컨버터 슬립의 평균은 거의 0 rpm 이다. 표준 편차는 1.1 rpm이다.
- 토크 컨버터 슬립의 부호 변동이 필터 적용 전 64%에서 필터 적용 후 26%로 감소하였다.
차속 구하기 - B: CAN 신호들에서 구하기¶
In [12]:df['slip_tc_filt'].describe()Out[12]:count 2.879300e+04 mean 8.022783e-03 std 1.097424e+00 min -1.398160e+01 25% -8.988829e-02 50% -3.938577e-72 75% 4.936604e-02 max 1.242765e+01 Name: slip_tc_filt, dtype: float64In [13]:def classify_slip_tc(slip, k_rpm_slip_acceleration=1.1, k_rpm_slip_deceleration=-1.1): ''' slip이 k_rpm_slip_acceleration 이상이면 가속으로 인한 슬립이 발생한 상태로 생각하고 sign_slip을 1로 한다. slip이 k_rpm_slip_deceleration 이하이면 감속으로 인한 슬립이 발생한 상태로 생각하고 sign_slip을 -1로 한다. 그 사이면 slip이 없는 상태로 간주하고 sign_slip을 0으로 한다. k_rpm_slip_acceleration과 k_rpm_slip_deceleration은 각각 가속과 감속으로 인한 슬립을 판단하는 기준값이다. slip_tc_filt의 표준 편차를 적용한다. ''' if slip > k_rpm_slip_acceleration: return 1 elif slip < k_rpm_slip_deceleration: return -1 else: return 0 df['sign_slip'] = df['slip_tc_filt'].apply(classify_slip_tc)In [14]:yaw_rate_summary = df['yaw_rate'].describe() yaw_rate_summaryOut[14]:count 28793.000000 mean 1.446362 std 4.728123 min -35.860000 25% 0.730000 50% 0.870000 75% 1.100000 max 35.330000 Name: yaw_rate, dtype: float64In [15]:def classify_turning(yaw_rate, yaw_rate_mean, yaw_rate_std): ''' abs(yaw_rate)가 k_deg_per_sec_turning 이상이면 선회라고 생각하고 is_turning을 True로 한다. 그렇지 않으면 False로 한다. k_deg_per_sec_turning은 선회 여부를 판단하는 기준값이다. yaw_rate의 표준 편차를 적용한다. ''' k_deg_per_sec_turning = yaw_rate_std * 0.5 # 필요에 따라 조정 return k_deg_per_sec_turning if abs(yaw_rate - yaw_rate_mean) >= k_deg_per_sec_turning else 0 yaw_rate_mean = df['yaw_rate'].mean() yaw_rate_std = df['yaw_rate'].std() df['is_turning'] = df['yaw_rate'].apply(classify_turning, args=(yaw_rate_mean, yaw_rate_std))In [16]:def calc_vs(df): ''' vehicle speed를 구한다. sign_slip이 0이면 slip이 없는 상태이므로, wheel speed의 평균을 vehicle speed로 간주한다. is_turning이 False 이고 sign_slip이 양수이면 가속으로 인한 슬립이 발생한 상태이므로, min(ws_fl, ws_fr, ws_rl, ws_rr)를 vehicle speed로 간주한다. is_turning이 True 이고 sign_slip이 양수이면 선회로 인한 슬립이 발생한 상태이므로, min(mean(ws_fl, ws_fr), mean(ws_rl, ws_rr))를 vehicle speed로 간주한다. is_turning이 False 이고 sign_slip이 음수이면 감속으로 인한 슬립이 발생한 상태이므로, max(ws_fl, ws_fr, ws_rl, ws_rr)를 vehicle speed로 간주한다. is_turning이 True 이고 sign_slip이 음수이면 선회로 인한 슬립이 발생한 상태이므로, max(mean(ws_fl, ws_fr), mean(ws_rl, ws_rr))를 vehicle speed로 간주한다. (추후에 추가할 수 있도록 주석으로 남겨둔다.) ''' df['vs'] = (df['ws_fl'] + df['ws_fr'] + df['ws_rl'] + df['ws_rr']) / 4 df.loc[(df['sign_slip'] > 0) & (df['is_turning'] == 0), 'vs'] = df[['ws_fl', 'ws_fr', 'ws_rl', 'ws_rr']].min(axis=1) df.loc[(df['sign_slip'] > 0) & (df['is_turning'] != 0), 'vs'] = df[['ws_fl', 'ws_fr']].mean(axis=1).combine(df[['ws_rl', 'ws_rr']].mean(axis=1), min) df.loc[(df['sign_slip'] < 0) & (df['is_turning'] == 0), 'vs'] = df[['ws_fl', 'ws_fr', 'ws_rl', 'ws_rr']].max(axis=1) df.loc[(df['sign_slip'] < 0) & (df['is_turning'] != 0), 'vs'] = df[['ws_fl', 'ws_fr']].mean(axis=1).combine(df[['ws_rl', 'ws_rr']].mean(axis=1), max) return df df = calc_vs(df)In [ ]:# vs, ws_fl, ws_fr, ws_rl, ws_rr와 yaw_rate와 slip_tc_filt를 서브프롯으로 그린다. fig = make_subplots(rows=3, cols=1, shared_xaxes=True, vertical_spacing=0.02) fig.add_trace(go.Scatter(x=df.index, y=df['vs'], mode='lines', name='vs'), row=1, col=1) fig.add_trace(go.Scatter(x=df.index, y=df['ws_fl'], mode='lines', name='ws_fl'), row=1, col=1) fig.add_trace(go.Scatter(x=df.index, y=df['ws_fr'], mode='lines', name='ws_fr'), row=1, col=1) fig.add_trace(go.Scatter(x=df.index, y=df['ws_rl'], mode='lines', name='ws_rl'), row=1, col=1) fig.add_trace(go.Scatter(x=df.index, y=df['ws_rr'], mode='lines', name='ws_rr'), row=1, col=1) fig.add_trace(go.Scatter(x=df.index, y=df['yaw_rate'], mode='lines', name='yaw_rate'), row=2, col=1) fig.add_trace(go.Scatter(x=df.index, y=df['is_turning'], mode='lines', name='is_turning'), row=2, col=1) fig.add_trace(go.Scatter(x=df.index, y=df['slip_tc_filt'], mode='lines', name='slip_tc_filt'), row=3, col=1) fig.add_trace(go.Scatter(x=df.index, y=df['sign_slip'], mode='lines', name='sign_slip'), row=3, col=1) fig.update_layout(width=1000,height=700, title_text="Vehicle Speed, Wheel Speeds, Yaw Rate, and Torque Converter Slip Over Time") fig.update_xaxes(rangeslider_visible=False, row=1, col=1) fig.update_xaxes(rangeslider_visible=False, row=2, col=1) fig.update_xaxes(rangeslider_visible=True, row=3, col=1) fig.show()차속 필터하기¶
In [18]:# 저역통과 필터 b_vs, a_vs = butter(N=2, Wn=0.8 / (fs / 2), btype='low') df['vs_filt'] = filtfilt(b_vs, a_vs, df['vs'].to_numpy())In [ ]:# vs와 vs_filt를 그래프로 비교한다. px.line( df, x=df.index, y=['vs', 'vs_filt'], title='Vehicle Speed: Raw vs Filtered', labels={'value': 'Speed (km/h)', 'ts': 'Time'}, width=1000, height=600 )발견 - vs_filt¶
- 확대하면 아래 그림과 같다.
- vs_filt를 사용하기로 한다.
- 현재 개념을 수립하는 단계이다. 위 필터가 왜 적합한지 따지는 것은 2차 관심사이다.
차속 구하기 - A: GPS 좌표를 이용하여 구한다.¶
In [20]:def haversine_distance(lat1, lon1, lat2, lon2): """ Calculate distance between two GPS coordinates using Haversine formula (in meters) Parameters: lat1, lon1: Latitude and longitude of first point (in degrees) lat2, lon2: Latitude and longitude of second point (in degrees) Returns: distance: Distance between two points (in meters) """ # Earth radius (meters) R = 6371000 # Convert degrees to radians lat1_rad = np.radians(lat1) lat2_rad = np.radians(lat2) delta_lat = np.radians(lat2 - lat1) delta_lon = np.radians(lon2 - lon1) # Haversine formula a = np.sin(delta_lat/2)**2 + np.cos(lat1_rad) * np.cos(lat2_rad) * np.sin(delta_lon/2)**2 c = 2 * np.arctan2(np.sqrt(a), np.sqrt(1-a)) distance = R * c return distanceIn [21]:# latitude, longitude 컬럼의 이웃한 좌표 간의 거리를 계산하여 distance 컬럼에 저장한다. # 좌표의 이전 행 데이터를 준비하기 위해 shift를 사용한다. df.loc[:, 'lat_prev'] = df.loc[:, 'latitude'].shift(1) df.loc[:, 'lon_prev'] = df.loc[:, 'longitude'].shift(1) # 첫 번째 행 제거 (이전 데이터 없음) df = df.dropna() df.loc[:, 'd_lat'] = df.loc[:, 'latitude'] - df.loc[:, 'lat_prev'] df.loc[:, 'd_lon'] = df.loc[:, 'longitude'] - df.loc[:, 'lon_prev']In [ ]:px.line( df, x=df.index, y=['d_lat', 'd_lon'], title='Change in Latitude and Longitude Over Time', labels={'value': 'Degrees', 'ts': 'Time'}, width=1000, height=600 )발견 - GPS 신호와 resample¶
- 확대하면 아래와 같다.
- GPS 좌표가 포함된 asc 파일을 mdf 파일로 변환할 때, 리샘플을 하였다. 리샘플의 주기가 실제 GPS 좌표 측정 주기보다 짧다. 새 행들이 추가되었고, 그 값들은 ffill로 채워졌다. 그 결과 새로 도입된 행들만큼 d_lat, d_long이 0인 행들이 있다.
- 가장 이상적인 방법은 asc를 mdf로 변환할 때, distance_gps와 speed_gps를 계산하는 것이다.
- 지금은 어떻게 처리하면 좋을까? ai와 대응책을 물었다. 아래와 같이 데이터 처리를 시도한다.
In [23]:# 가정: df 인덱스가 datetime, 컬럼은 latitude/longitude # 1) 실제 GPS 갱신 여부 판단 (부동소수 오차 대응용 tolerance) eps = 1e-10 gps_changed = ( (df["latitude"].diff().abs() > eps) | (df["longitude"].diff().abs() > eps) ) # 첫 행은 기준점으로 포함 gps_changed.iloc[0] = True gps = df.loc[gps_changed, ["latitude", "longitude"]].copy() # 2) GPS 갱신 시점끼리 거리 계산 gps["lat_prev"] = gps["latitude"].shift(1) gps["lon_prev"] = gps["longitude"].shift(1) gps["distance_m"] = haversine_distance( gps["lat_prev"].values, gps["lon_prev"].values, gps["latitude"].values, gps["longitude"].values ) gps["distance_m"] = gps["distance_m"].fillna(0) # 3) GPS 갱신 간 dt로 속도 계산 idx_series = gps.index.to_series() if pd.api.types.is_datetime64_any_dtype(gps.index) or pd.api.types.is_timedelta64_dtype(gps.index): dt = idx_series.diff().dt.total_seconds() else: dt = pd.Series(pd.to_numeric(pd.Index(gps.index), errors="coerce"), index=gps.index).diff() dt = dt.replace(0, np.nan) gps["vs_gps_mps"] = (gps["distance_m"] / dt).replace([np.inf, -np.inf], np.nan) gps["vs_gps_mps"] = gps["vs_gps_mps"].fillna(0) # 4) 전체 리샘플 프레임으로 매핑 df["distance_m_step"] = 0.0 df.loc[gps.index, "distance_m_step"] = gps["distance_m"] df["distance_m_cum"] = df["distance_m_step"].cumsum() # 속도 표현 선택: # A) 계단형 유지(마지막 GPS 속도 유지) df["vs_gps_kph_ffill"] = np.nan df.loc[gps.index, "vs_gps_kph_ffill"] = gps["vs_gps_mps"] * 3.6 df["vs_gps_kph_ffill"] = df["vs_gps_kph_ffill"].ffill().fillna(0) # B) 선형 보간(원하면) df["vs_gps_kph_interp"] = np.nan df.loc[gps.index, "vs_gps_kph_interp"] = gps["vs_gps_mps"] * 3.6 df["vs_gps_kph_interp"] = df["vs_gps_kph_interp"].interpolate().fillna(0)In [ ]:# GPS 속도 비교 그래프 px.line( df, x=df.index, y=['vs_filt', 'vs_gps_kph_ffill', 'vs_gps_kph_interp'], title='GPS Speed: Forward-Filled vs Interpolated', labels={'value': 'Speed (kph)', 'ts': 'Time'}, width=1000, height=600 )발견 - vs_gps¶
- 시동을 걸면 Tlog100x가 즉시 데이터 저장을 시작한다.
- 지하 주차장에서 출발하여 지상으로 올라와서 Tlog100x가 GPS 신호를 잡을 때까지 차속을 계산할 수 없다는 단점이 있다.
- 계산된 차속이 거칠고, 정차(0kph) 할 때, 차속 왜곡 기간이 지나치게 길 수 있다.
- asc -> mdf 변환 시에 차속 계산을 하는 것으로 하고, 여기서는 CAN 신호 기반 차속 신호를 이용한다.
바퀴 슬립율 구하기¶
In [25]:df['r_slip_fl'] = (df['ws_fl'] - df['vs_filt']) / df['vs_filt'] * 100 df['r_slip_fr'] = (df['ws_fr'] - df['vs_filt']) / df['vs_filt'] * 100 df['r_slip_rl'] = (df['ws_rl'] - df['vs_filt']) / df['vs_filt'] * 100 df['r_slip_rr'] = (df['ws_rr'] - df['vs_filt']) / df['vs_filt'] * 100 # vs_filt가 낮은 경우, slip 계산이 불안정할 수 있으므로, vs_filt가 k_speed_too_low 미만인 경우 slip을 0으로 간주한다. k_speed_too_low = 2.5 # km/h, 필요에 따라 조정 df.loc[df['vs_filt'] < k_speed_too_low, ['r_slip_fl', 'r_slip_fr', 'r_slip_rl', 'r_slip_rr']] = 0In [ ]:# slip_fl, slip_fr, slip_rl, slip_rr를 서브플롯으로 그린다. fig = make_subplots(rows=5, cols=1, shared_xaxes=True, vertical_spacing=0.02) fig.add_trace(go.Scatter(x=df.index, y=df['r_slip_fl'], mode='lines', name='slip_fl'), row=1, col=1) fig.add_trace(go.Scatter(x=df.index, y=df['r_slip_fr'], mode='lines', name='slip_fr'), row=2, col=1) fig.add_trace(go.Scatter(x=df.index, y=df['r_slip_rl'], mode='lines', name='slip_rl'), row=3, col=1) fig.add_trace(go.Scatter(x=df.index, y=df['r_slip_rr'], mode='lines', name='slip_rr'), row=4, col=1) fig.add_trace(go.Scatter(x=df.index, y=df['yaw_rate'], mode='lines', name='yaw_rate'), row=5, col=1) fig.update_layout(width=1000, height=1100, title_text="Wheel Slip Ratios Over Time") fig.update_xaxes(rangeslider_visible=True, row=5, col=1) fig.show()발견 - 슬립률과 선회¶
- 선회 구간에서 바퀴 슬립율이 높다.
- 선회 시에는 가중치를 낮추야 하는 것인가?
타이어 마모 인덱스¶
- 타이어는 마모 인덱스라는 것을 만든다.
- 타이어마다 개별적으로 있어야 하겠지.
- 슬립율을 적분해서 만들면 될 것 같다.
- 슬립율은 음수여도 인덱스는 증가하도록 계산식을 만들어야 한다.
- 슬립율에 가중치가 적용되어 마모 인덱스가 계산되도록 해야겠다.
In [46]:# 선회 중 바퀴 슬립률의 통계량을 구한다. turning_slip_stats = df[df['is_turning'] != 0][['r_slip_fl', 'r_slip_fr', 'r_slip_rl', 'r_slip_rr']].describe() turning_slip_statsOut[46]:r_slip_fl r_slip_fr r_slip_rl r_slip_rr count 3654.000000 3654.000000 3654.000000 3654.000000 mean -0.601213 2.703639 -2.606825 0.868139 std 4.560471 5.562668 5.849281 4.666240 min -12.366665 -9.246471 -30.474144 -22.942318 25% -3.373488 -0.202702 -4.952815 -0.622306 50% -0.146422 0.594971 -0.613929 0.148466 75% 0.615643 4.743949 0.188193 3.330762 max 29.132756 27.226436 17.747275 17.527176 In [28]:# 선회가 아닌 중 바퀴 슬립률의 통계량을 구한다. non_turning_slip_stats = df[df['is_turning'] == 0][['r_slip_fl', 'r_slip_fr', 'r_slip_rl', 'r_slip_rr']].describe() non_turning_slip_statsOut[28]:r_slip_fl r_slip_fr r_slip_rl r_slip_rr count 25138.000000 25138.000000 25138.000000 25138.000000 mean 0.129247 0.192052 -0.180969 -0.123421 std 0.493932 0.528245 0.520399 0.511055 min -7.891926 -7.877434 -12.601392 -9.648983 25% 0.000000 0.000000 -0.338130 -0.273805 50% 0.000000 0.041585 -0.073588 -0.015805 75% 0.284475 0.357648 0.000000 0.000000 max 15.463468 12.169217 12.133596 11.509765 In [29]:turning_slip_stats.loc['mean'] / non_turning_slip_stats.loc['mean']Out[29]:r_slip_fl NaN r_slip_fr NaN r_slip_rl NaN r_slip_rr NaN Name: mean, dtype: float64발견 - turning¶
- turning 일 때 r_slip_xx가 turning이 아닐 때 대비 차이가 크다.
- turning 일 때 r_slip_xx를 제한한다.
In [30]:# 튜닝 파라미터 k_r_slip_deadzone = 0.01 # 마모가 없다고 설정한 슬립 비율 기준 k_slip_power = 2.0 # >1 이면 큰 슬립에 더 큰 패널티 k_speed_min_kmh = 2.5 # 저속 구간 제외 기준 k_limit_factor_r_slip_in_turning = 2 # 선회 중 슬립이 과대평가되는 것을 보정하는 계수 (필요에 따라 조정 가능) def wear_increment_from_slip(r_slip_xx: pd.Series, r_slip_max_no_turning: float) -> pd.Series: ''' slip_series: 슬립 비율 시리즈 (예: r_slip_fl) 앞뒤 바퀴의 수직 하중 차이가 고려되야 한다. 동일한 슬립율이라도 수직 하중이 큰 바퀴에서의 마모 증가량이 더 클 것이다. 앞 바퀴 하중 : 뒷 바퀴 하중 = 6 : 4 로 한다. 즉, 앞바퀴는 슬립으로 인한 마모 증가량이 1.5배가 된다. (필요에 따라 조정 가능) return: 슬립으로 인한 마모 증가량 시리즈 (0 이상) ''' # 1) 절대값 사용: 양/음 슬립 모두 마모 증가 r_slip_abs = r_slip_xx.abs() # 2) 데드존: 미소 슬립 노이즈 제거 r_slip_eff = (r_slip_abs - k_r_slip_deadzone).clip(lower=0.0) # 3) 선회 중 r_slip이 크게 계산된다. # 선회 여부 시 r_slip_eff를 보정한다. r_slip_eff = r_slip_eff.where(df['is_turning'] > 0, r_slip_max_no_turning * k_limit_factor_r_slip_in_turning) # 4) 수직 하중 가중 (앞바퀴 1.5배, 뒷바퀴 1.0배) if 'fl' in r_slip_xx.name: r_slip_eff *= 1.5 elif 'rl' in r_slip_xx.name: r_slip_eff *= 1.0 elif 'fr' in r_slip_xx.name: r_slip_eff *= 1.5 elif 'rr' in r_slip_xx.name: r_slip_eff *= 1.0 # 5) 비선형 가중 severity = r_slip_eff.pow(k_slip_power) # 6) 시간 적분 inc = severity * dt return inc.where(valid_speed, 0.0)In [31]:# 인덱스에서 dt[s] 계산 (timedelta index / numeric index 모두 지원) didx = df.index.to_series().diff() if pd.api.types.is_timedelta64_dtype(didx): dt = didx.dt.total_seconds().fillna(0.0) else: dt = didx.astype(float).fillna(0.0) # 유효 속도 마스크 valid_speed = df["vs_filt"] >= k_speed_min_kmh # 타이어별 누적 인덱스 계산 for wheel in ["fl", "fr", "rl", "rr"]: col_r_slip = f"r_slip_{wheel}" col_inc = f"wear_inc_{wheel}" col_idx = f"wear_idx_{wheel}" r_slip_max_no_turning = df.loc[df['is_turning'] == 0, col_r_slip].max() df[col_inc] = wear_increment_from_slip(df[col_r_slip], r_slip_max_no_turning) df[col_idx] = df[col_inc].cumsum()In [32]:def plot_wear_index(df): color_map = { "fl": "#1f77b4", "fr": "#d62728", "rl": "#2ca02c", "rr": "#ff7f0e" } fig = make_subplots( rows=3, cols=1, shared_xaxes=True, vertical_spacing=0.08, subplot_titles=( "Vehicle Speed (filtered)", "Wheel Slip Ratios", "Tire Wear Estimation Index" ) ) fig.add_trace( go.Scatter( x=df.index, y=df["vs_filt"], mode="lines", name="vs_filt", line=dict(color="#444444", width=2) ), row=1, col=1 ) for wheel in ["fl", "fr", "rl", "rr"]: fig.add_trace( go.Scatter( x=df.index, y=df[f"r_slip_{wheel}"], mode="lines", name=f"slip_{wheel}", line=dict(color=color_map[wheel], width=1.8) ), row=2, col=1 ) for wheel in ["fl", "fr", "rl", "rr"]: fig.add_trace( go.Scatter( x=df.index, y=df[f"wear_idx_{wheel}"], mode="lines", name=f"wear_idx_{wheel}", line=dict(color=color_map[wheel], width=2.2) ), row=3, col=1 ) fig.update_yaxes(title_text="km/h", row=1, col=1) fig.update_yaxes(title_text="slip ratio", row=2, col=1) fig.update_yaxes(title_text="wear index", row=3, col=1) # rangeslider를 맨 아래 서브플롯에만 표시 fig.update_xaxes(rangeslider_visible=False, row=1, col=1) fig.update_xaxes(rangeslider_visible=False, row=2, col=1) fig.update_xaxes(rangeslider_visible=False, row=3, col=1) fig.update_layout( width=1000, height=600, title_text="Vehicle Speed and Tire Wear Estimation", legend_title_text="Signals" ) return figIn [ ]:plot_wear_index(df).show()타이어 마모 인덱스와 고도¶
In [34]:def plot_wear_index_with_altitude(df): # altitude와 wear_idx_fl의 관계를 산점도로 그려본다. # 두 그래프의 스케이일이 많이 다르므로 y축을 각각 따로 만들어서 그려본다. fig = make_subplots( rows=1, cols=1, shared_xaxes=True, specs=[[{"secondary_y": True}]], subplot_titles=["Altitude vs Front Left Tire Wear Index"] ) fig.add_trace( go.Scatter( x=df.index, y=df["altitude"], mode="lines", name="altitude", line=dict(color="#888888", width=2) ), row=1, col=1, secondary_y=False ) fig.add_trace( go.Scatter( x=df.index, y=df["wear_idx_fl"], mode="lines", name="wear_idx_fl", line=dict(color="#1f77b4", width=2) ), row=1, col=1, secondary_y=True ) fig.update_yaxes(title_text="Altitude (m)", row=1, col=1, secondary_y=False) fig.update_yaxes(title_text="Front Left Tire Wear Index", row=1, col=1, secondary_y=True) fig.update_layout( width=1000, height=600, title_text="Altitude and Front Left Tire Wear Index Over Time", legend_title_text="Signals" ) return figIn [ ]:plot_wear_index_with_altitude(df).show()발견 - 산을 넘을 때 타이어가 많이 닿지 않을까?¶
- 그래프를 보면 1500 ~ 2000초 구간에서 산을 넘어간다. 나는 이 구간에서 타이어 마모가 많을 것으로 생각했다. 그런데 타이어 마모 인덱스를 보면 인덱스의 증가가 거의 없다.
- 경사를 마모에 반영해 보자.
경사도 구하기¶
- vs_filt를 미분하여 종가속도 long_accel_vs를 구한다.
- 측정된 신호 long_accel과 long_accel_vs의 차이 diff_long_accel을 구한다.
- 이 차이를 경사도로 볼 수 있을까?
In [ ]:px.line( df, x=df.index, y='long_accel', title='Longitudinal Acceleration Over Time', labels={'long_accel': 'Longitudinal Acceleration (m/s²)', 'ts': 'Time'}, width=1000, height=600 )발견 - long_accel 노이즈¶
- long_accel에 노이즈가 많다. 필터링한다.
In [37]:# long_accel에 노이즈가 많다. 필터링한다. b_la, a_la = butter(N=3, Wn=1.0 / (fs / 2), btype='low') df['long_accel_filt'] = filtfilt(b_la, a_la, df['long_accel'].to_numpy()) # vs_filt를 미분하여 종가속도 long_accel_vs를 구한다. df['long_accel_vs'] = df['vs_filt'].mul(1000 / 3600).diff() / dt # 측정된 신호 long_accel과 long_accel_vs의 차이 diff_long_accel을 구한다. df['diff_long_accel'] = df['long_accel_filt'] - df['long_accel_vs'] # 이 차이를 경사도 지표로 볼 수 있을까? df['diff_long_accel'] = df['diff_long_accel'].fillna(df['diff_long_accel'].mean()) # 첫 번째 값은 diff 계산이 안되므로 두 번째 값으로 채운다. # diff_long_accel에도 노이즈가 많다. 필터링한다. b_la, a_la = butter(N=4, Wn=0.1 / (fs / 2), btype='low') df['diff_long_accel_filt'] = filtfilt(b_la, a_la, df['diff_long_accel'].to_numpy())In [ ]:px.line( df, x=df.index, y=['long_accel', 'long_accel_filt', 'long_accel_vs'], title='Longitudinal Acceleration Over Time', labels={'long_accel_filt': 'Longitudinal Acceleration (m/s²)', 'long_accel': 'Longitudinal Acceleration (m/s²)', 'ts': 'Time'}, width=1000, height=600 )발견 - long_accel_filt¶
- 그래프를 확대하면 아래와 같다.
- 필터를 적용한 long_accel_filt가 적당히 매끄러워 보인다.
- 이 신호를 사용하기로 한다.
In [39]:def plot_diff_long_accel_and_altitude(df): # long_accel, long_accel_vs, diff_long_accel, altitude를 3 x 1 subplot으로 그려본다. fig = make_subplots( rows=3, cols=1, shared_xaxes=True, subplot_titles=["Longitudinal Acceleration: Measured vs Estimated", "Difference in Longitudinal Acceleration", "Altitude"] ) fig.add_trace( go.Scatter( x=df.index, y=df["long_accel_filt"], mode="lines", name="long_accel_filt (measured)", line=dict(color="#d62728", width=2) ), row=1, col=1 ) fig.add_trace( go.Scatter( x=df.index, y=df["long_accel_vs"], mode="lines", name="long_accel_vs (from vehicle speed)", line=dict(color="#1f77b4", width=2) ), row=1, col=1 ) fig.add_trace( go.Scatter( x=df.index, y=df["diff_long_accel_filt"], mode="lines", name="diff_long_accel_filt", line=dict(color="#2ca02c", width=2) ), row=2, col=1 ) fig.add_trace( go.Scatter( x=df.index, y=df["altitude"], mode="lines", name="altitude", line=dict(color="#888888", width=1.5, dash='dot') ), row=3, col=1 ) fig.update_yaxes(title_text="Longitudinal Acceleration (m/s²)", row=1, col=1) fig.update_yaxes(title_text="Difference in Longitudinal Acceleration (m/s²)", row=2, col=1) fig.update_yaxes(title_text="Altitude (m)", row=3, col=1) fig.update_layout( width=1000, height=600, title_text="Longitudinal Acceleration Comparison", legend_title_text="Signals" ) return figIn [ ]:plot_diff_long_accel_and_altitude(df).show()발견 - 경사와 종가속도¶
- 산을 넘을 때, 즉, 경사가 있을 때, 차속에서 계산한 종가속도와 센서가 측정한 종가속도 차이가 두두러진다.
타이어 마모 인덱스에 경사도 반영하기¶
In [41]:df['diff_long_accel_filt'].describe()Out[41]:count 28792.000000 mean -0.100138 std 0.300798 min -1.110021 25% -0.204642 50% -0.081113 75% -0.022774 max 1.636077 Name: diff_long_accel_filt, dtype: float64In [42]:# 튜닝 파라미터 k_factor_slope_deadzone = 0.5 # 경사도 가중의 데드존 (경사도가 이 값 이하이면 가중치 0) k_factor_slope_to_weight = 1 def wear_increment_from_slip_w_slope(r_slip_xx: pd.Series, r_slip_max_no_turning: float) -> pd.Series: ''' slip_series: 슬립 비율 시리즈 (예: r_slip_fl) 앞뒤 바퀴의 수직 하중 차이가 고려되야 한다. 동일한 슬립율이라도 수직 하중이 큰 바퀴에서의 마모 증가량이 더 클 것이다. 앞 바퀴 하중 : 뒷 바퀴 하중 = 6 : 4 로 한다. 즉, 앞바퀴는 슬립으로 인한 마모 증가량이 1.5배가 된다. (필요에 따라 조정 가능) 경사가 심한 구간에서는 구동륜의 마모가 가속된다고 가정할 수 있다. 이 경우, 경사도에 따른 가중치를 추가할 수 있다. return: 슬립으로 인한 마모 증가량 시리즈 (0 이상) ''' # 1) 절대값 사용: 양/음 슬립 모두 마모 증가 s_abs = r_slip_xx.abs() # 2) 데드존: 미소 슬립 노이즈 제거 s_eff = (s_abs - k_r_slip_deadzone).clip(lower=0.0) # 3) 선회 중 r_slip이 크게 계산된다. # 선회 여부 시 r_slip_eff를 보정한다. s_eff = s_eff.where(df['is_turning'] > 0, r_slip_max_no_turning * k_limit_factor_r_slip_in_turning) # 4) 수직 하중 가중 # 앞바퀴 1.5배, 뒷바퀴 1.0배: 베뉴의 경우, 앞바퀴 하중 : 뒷바퀴 하중 = 6 : 4 정도로 추정된다. (필요에 따라 조정 가능) if 'fl' in r_slip_xx.name: s_eff *= 1.5 elif 'rl' in r_slip_xx.name: s_eff *= 1.0 elif 'fr' in r_slip_xx.name: s_eff *= 1.5 elif 'rr' in r_slip_xx.name: s_eff *= 1.0 # 5) 경사도 가중 (경사도가 클수록 마모 증가) # 경사도는 diff_long_accel_filt로 대체한다. (필요에 따라 조정 가능) # 구동륜에만 적용한다. 베뉴는 전륜구동이므로, fl과 fr에 적용한다. (필요에 따라 조정 가능) if 'fl' in r_slip_xx.name or 'fr' in r_slip_xx.name: slope = df['diff_long_accel_filt'] slope_mean = slope.mean() slope_std = slope.std() slope_abs = (slope - slope_mean).abs() slope_eff = (slope_abs - slope_std * k_factor_slope_deadzone).clip(lower=0.0) slope_weight = 1 + slope_eff * k_factor_slope_to_weight s_eff *= slope_weight # 6) 비선형 가중 severity = s_eff.pow(k_slip_power) # 7) 시간 적분 inc = severity * dt return inc.where(valid_speed, 0.0)In [43]:# 타이어별 누적 인덱스 계산 for wheel in ["fl", "fr", "rl", "rr"]: col_r_slip = f"r_slip_{wheel}" col_inc = f"wear_inc_{wheel}" col_idx = f"wear_idx_{wheel}" r_slip_max_no_turning = df.loc[df['is_turning'] == 0, col_r_slip].max() df[col_inc] = wear_increment_from_slip_w_slope(df[col_r_slip], r_slip_max_no_turning) df[col_idx] = df[col_inc].cumsum()In [ ]:plot_wear_index_with_altitude(df).show()발견 - 경사의 타이어 마모 인덱스에 영향¶
- 아래는 경사를 고려하기 전 타이어 마모 인덱스 그래프이다.
- 그래프들을 비교하면 산을 넘을 때 타이어 마모 인덱스가 증가한 것을 확인할 수 있다.
주행 경로와 타이어 마모 인덱스를 지도에서 시각화¶
In [ ]:# 경로와 fl 바퀴의 마모 인덱스를 지도에 시각화한다. # 마모인덱스가 잘 보이도록 지도는 흑백(그레이스케일) 스타일을 사용한다. # scatter_map을 사용한다. (scatter_mapbox 사용 안 함) import plotly.express as px # 지도 시각화를 위해 latitude, longitude, wear_idx_fl 컬럼을 사용한다. wear_idx_fl_min = df['wear_idx_fl'].min() wear_idx_fl_max = df['wear_idx_fl'].max() wear_idx_range = wear_idx_fl_max - wear_idx_fl_min if wear_idx_fl_max > wear_idx_fl_min else 1.0 df['wear_idx_fl_normalized'] = (df['wear_idx_fl'] - wear_idx_fl_min) / wear_idx_range # df['wear_color'] = wear_idx_fl_normalized.apply( # lambda x: f"rgb({int(255*x)}, {int(255*(1-x))}, 126)" # ) fig = px.scatter_map( df, lat='latitude', lon='longitude', color='wear_idx_fl_normalized', color_continuous_scale=[ (0.0, 'green'), (0.5, 'yellow'), (1.0, 'red') ], range_color=(0, 1), size_max=15, zoom=10, map_style='carto-positron', # 흑백 스타일 title='Front Left Tire Wear Index Along the Route' ) fig.update_layout( coloraxis_colorbar=dict( title='Normalized Wear Index', len=0.5 ), width=900, height=900 ) fig.show()결론¶
- CAN 데이터와 GPS 데이터를 함께 이용하면 뭔가 유용한 계산을 할 수 있지 않을까 해서 타이어 마모 인덱스라는 것을 정의하고 계산해 봤다.
- 내가 정의한 타이어 마모 인덱스는 이대로는 현실적 유용성은 없다고 생각한다. 인텍스를 정의하고 계산식을 만드는 과정이 누군가에게 영감을 줄 수 있으면 좋겠다.
'application' 카테고리의 다른 글
blf & dbc --> mdf or csv 개선 (0) 2026.04.03 dbc 병합 (1) 2026.03.16 blf & dbc --> mdf or csv 변환 (0) 2026.03.10 blf & dbc --> mdf 변환 (1) 2026.03.08 바이브 코딩으로 진단기 만들기 (0) 2026.01.19 - 타이어는 언제 많이 마모될까?