Dacon 월간 데이콘 항공편 지연 예측 AI 경진대회
ID
: 샘플 고유 id
Month
: 해당 항공편의 출발 월
Day_of_Month
: Month에 해당하는 월의 날짜
Estimated_Departure_Time
: 전산 시스템을 바탕으로 측정된 비행기의 출발 시간 (현지 시각, HH:MM 형식)
Estimated_Arrival_Time
: 전산 시스템을 바탕으로 측정된 비행기의 도착 시간 (현지 시각, HH:MM 형식)
Cancelled
: 해당 항공편의 취소 여부 (0: 취소되지 않음, 1: 취소됨)
Diverted
: 해당 항공편의 경유 여부 (0: 취소되지 않음, 1: 취소됨)
Origin_Airport
: 해당 항공편 출발 공항의 고유 코드 (IATA 공항 코드)
Origin_Airport_ID
: 해당 항공편 출발 공항의 고유 ID (US DOT ID)
Origin_State
: 해당 항공편 출발 공항이 위치한 주의 이름
Destination_Airport
: 해당 항공편 도착 공항의 고유 코드 (IATA 공항 코드)
Destination_Airport_ID
: 해당 항공편 도착 공항의 고유 ID (US DOT ID)
Destination_State
: 해당 항공편 도착 공항이 위치한 주의 이름
Distance
: 출발 공항과 도착 공항 사이의 거리 (mile 단위)
Airline
: 해당 항공편을 운항하는 항공사
Carrier_Code(IATA)
: 해당 항공편을 운항하는 항공사의 고유 코드
(IATA 공항 코드, 단 다른 항공사가 같은 코드를 보유할 수도 있음)
Carrier_ID(DOT)
: 해당 항공편을 운항하는 항공사의 고유 ID (US DOT ID)
Tail_Number
: 해당 항공편을 운항하는 항공기의 고유 등록번호
Delay
: 항공편 지연 여부 (Not_Delayed, Delayed)
시간 : Month
, Estimated_Departure_Time
,
거리 : Distance
출발 및 도착지 : Origin_Airport_ID
, Destination_Airport_ID
항공사 : Carrier_ID(DOT)
Estimated_Departure_Time
, Carrier_ID(DOT)
Estimated_Departure_Time
아침에 출발하냐 저녁에 출발하냐 등 다양한 지연 사유가 있을 거라 생각해 단순히 최빈값으로 채우지 않았다.
Estimated_Departure_Time
와 Estimated_Arrival_Time
둘 다 결측치인 경우Estimated_Departure_Time
의 값을 최빈값으로
for index, row in train.iterrows():
if pd.isnull(row['Estimated_Arrival_Time']) and pd.isnull(row['Estimated_Departure_Time']):
train.at[index, 'Estimated_Departure_Time'] = train['Estimated_Departure_Time'].mode().iloc[0]
Estimated_Departure_Time
와 Estimated_Arrival_Time
중 하나만 결측치인 경우Estimated_Departure_Time
과 Estimated_Arrival_Time
의 형태를 HHMM
에서 분(minute)으로 바꾸고 새로운 Column인 Estimated_Departure_Minutes
와 Estimated_Arrival_Minutes
을 생성하고 두 컬럼의 차이인 운항시간 Flight_Time
컬럼까지 생성했다.
train['Flight_Time'] = None
train['Estimated_Departure_Minutes'] = None
train['Estimated_Arrival_Minutes'] = None
for index, row in train.iterrows():
dep_time = row['Estimated_Departure_Time']
arr_time = row['Estimated_Arrival_Time']
if arr_time == 0 and dep_time == 0:
continue
elif dep_time == 0:
arr_time_in_minutes = convert_to_minutes(arr_time)
train.at[index, 'Estimated_Arrival_Minutes'] = arr_time_in_minutes
elif arr_time == 0:
dep_time_in_minutes = convert_to_minutes(dep_time)
train.at[index, 'Estimated_Departure_Minutes'] = dep_time_in_minutes
else:
dep_time_in_minutes = convert_to_minutes(dep_time)
arr_time_in_minutes = convert_to_minutes(arr_time)
if arr_time_in_minutes < dep_time_in_minutes:
arr_time_in_minutes += 1440
duration_in_minutes = arr_time_in_minutes - dep_time_in_minutes
train.at[index, 'Flight_Time'] = duration_in_minutes
train.at[index, 'Estimated_Arrival_Minutes'] = arr_time_in_minutes
train.at[index, 'Estimated_Departure_Minutes'] = dep_time_in_minutes
이후 Flight_Time
과 Distance
를 활용해 분당 거리(mile) 인 Flight_Time_per_Distance
새 컬럼을 생성했다.
for index, row in train.iterrows():
if pd.isnull(row['Flight_Time']):
train.at[index, 'Flight_Time_per_Distance'] = None
else:
flight_time_per_distance = row['Flight_Time'] / row['Distance']
train.at[index, 'Flight_Time_per_Distance'] = flight_time_per_distance
Flight_Time_per_Distance
컬럼의 결측치가 없는 로우들의 평균을 구했다.
avg_list = []
for index, row in train.iterrows():
if pd.isnull(row['Flight_Time_per_Distance']):
pass
else:
avg_list.append(row['Flight_Time_per_Distance'])
velocity_avg = sum(avg_list) / len(avg_list)
이후 velocity_avg
값과 결측치가 있는 Flight_Time
로우의 Distance
컬럼의 값을 곱해 Flight_Time
값을 채워줬다.
for index, row in train.iterrows():
if row['Flight_Time'] is None:
train.at[index, 'Flight_Time'] = row['Distance'] * velocity_avg
else:
pass
전처리 과정 중 새로 만든 Flight_Time
컬럼은 단거리, 중거리, 장거리 등의 운항에 따른 연착 이유(사실상 Distance와 같은 이유)로 Feature로 선정했다.
Carrier_ID(DOT)
항공사 별로 고유 코드가 있는게 아니라 같은 코드여도 다른 항공사인 경우가 많았다.
Taile_Number
컬럼과 Carrier_Code(IATA)
의 상관관계를 통해 Carrier_ID(DOT)
을 채웠다.
예를 들면 Taile_Number
의 마지막 알파벳이 SY 이며 Carrier_Code(IATA)
가 UA 이면
Carrier_ID(DOT)
은 20304 인 상관관계가 있었다.
먼저 Taile_Number
의 뒤 알파벳들을 추출하고 새로운 컬럼을 만들어줬다.
tail_numbers = train['Tail_Number']
tail_alpha = []
for tail_number in tail_numbers:
match = re.search(r'\d([A-Za-z]+)$', tail_number)
if match:
tail_alpha.append(match.group(1))
else:
tail_alpha.append(None)
train['tail_alpha'] = tail_alpha
이후 Carrier_Code(IATA)
와 Taile_Number
의 매핑 딕셔너리를 만든 후 둘을 비교하며 딕셔너리를 만들었다.
IATA_tail = {}
for i in train['Carrier_Code(IATA)'].unique():
for j in train['tail_alpha'].unique():
df = train[(train['tail_alpha'] == j) & (train['Carrier_Code(IATA)'] == i)]
if df.shape[0] > 0:
if len(df['Carrier_ID(DOT)'].unique()) >= 3:
IATA_tail[i] = j
else:
fill = df['Carrier_ID(DOT)'].max()
train.loc[df.index, 'Carrier_ID(DOT)'] = fill
이렇게 해도 Carrier_ID(DOT
컬럼에 11883개(약 1%)의 결측치가남아있었는데 이는 Airline
컬럼과 매칭해 채워줬다.
pairs = list(zip(train['Airline'], train['Carrier_ID(DOT)']))
unique_pairs = {}
for pair in pairs:
if not any(pd.isna(p) for p in pair):
unique_pairs[pair] = None
unique_pairs_list = list(unique_pairs.keys())
air_dict = {}
for i in unique_pairs_list:
air_dict[i[0]] = i[1]
for index, row in train.iterrows():
if pd.isna(row['Carrier_ID(DOT)']):
airline = row['Airline']
if airline in air_dict:
train.loc[index, 'Carrier_ID(DOT)'] = air_dict[airline]
위 과정 이후 4462개의 결측치가 남아있었는데 채워진Carrier_Code(IATA)
와 tail_alpha
의 매핑 딕셔너리를 만든 후 결측치를
iata_alph = {}
for index, row in train.iterrows():
if pd.isna(row['Carrier_ID(DOT)']):
pass
elif pd.isna(row["Carrier_Code(IATA)"]):
pass
elif pd.isna(row['tail_alpha']):
pass
else:
if row["Carrier_Code(IATA)"] not in iata_alph:
iata_alph[(row["Carrier_Code(IATA)"], row['tail_alpha'])] = row['Carrier_ID(DOT)']
else:
pass
for index, row in train.iterrows():
if pd.isna(row['Carrier_ID(DOT)']):
iata = row['Carrier_Code(IATA)']
alpha = row['tail_alpha']
if (iata, alpha) in iata_alph:
dot = iata_alph[(iata, alpha)]
train.at[index, 'Carrier_ID(DOT)'] = dot
else:
pass
위 과정 이후 2450개의 결측치는 더이상 채울 단서가 없어 최빈값으로 채워줬다.
시차가 적용 됐는지 Distance
는 길지만 Flight_Time
이 몇분 안나온 경우가 있었다. (e.g. 0530 출발, 0545도착, 800 마일)
아마 출발지와 도착지 각각의 시차가 적용된게 아닐까? 라는 생각에 시차 정보 크롤링 후 새로운 시차 컬럼을 만들고, 시차에 따른 출발시간과 도착시간을 만들고 학습을 돌렸으나 logloss가 현저히 낮게 나와 포기...
사실 준지도학습이 제일 문제였다. (할줄모름)
이 경우 구글링과 챗 GPT를 활용해 진행했다.
from sklearn.mixture import GaussianMixture
# Semi-supervised learning
gm_model = GaussianMixture(n_components=2)
gm_model.fit(semi_train[features], semi_train[target_variable])
predictions3 = gm_model.predict(semi_test[features])
from sklearn.cluster import MiniBatchKMeans
# Semi-supervised learning
mbk_model = MiniBatchKMeans(n_clusters=2)
mbk_model.fit(semi_train[features], semi_train[target_variable])
predictions = mbk_model.predict(semi_test[features])
위 준지도학습 모델의 경우 MiniBatchKMeans
의 경우 logloss 값이 적게 나와 MiniBatchKMeans
를 채택했다.