Dacon 월간 데이콘 항공편 지연 예측 AI 경진대회

기준맨·2023년 5월 18일
0

Dacon

목록 보기
1/1

Dacon 월간 데이콘 항공편 지연 예측 AI 경진대회

Dacon 월간 데이콘 항공편 지연 예측 AI 경진대회

결과

Public

Private

Data

Columns

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)

Select Feature

시간 : Month, Estimated_Departure_Time,
거리 : Distance
출발 및 도착지 : Origin_Airport_ID, Destination_Airport_ID
항공사 : Carrier_ID(DOT)

Null Check

is Null

Estimated_Departure_Time, Carrier_ID(DOT)

결측치 처리

Estimated_Departure_Time

아침에 출발하냐 저녁에 출발하냐 등 다양한 지연 사유가 있을 거라 생각해 단순히 최빈값으로 채우지 않았다.

case 1. Estimated_Departure_TimeEstimated_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]

case 2. Estimated_Departure_TimeEstimated_Arrival_Time 중 하나만 결측치인 경우

Estimated_Departure_TimeEstimated_Arrival_Time 의 형태를 HHMM 에서 분(minute)으로 바꾸고 새로운 Column인 Estimated_Departure_MinutesEstimated_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_TimeDistance 를 활용해 분당 거리(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가 현저히 낮게 나와 포기...

해당 전처리 노트북 github

Semi SuperVised Learning

사실 준지도학습이 제일 문제였다. (할줄모름)
이 경우 구글링과 챗 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를 채택했다.

Learning .XGB

XGBoost Learning

Github repo

github

0개의 댓글