[Python 실습] 유성우가 언제 떨어질까? 데이터로 예측해보기

감자·2026년 1월 13일

microsoft_dataschool

목록 보기
5/28
post-thumbnail

01. 개요

혜성이 지나간 자리에 남은 유성체(Meteoroid)들이 지구 공전 궤도와 만나면 유성우(Meteor Shower)가 발생합니다. 하지만 유성우가 떨어진다고 해서 언제나 볼 수 있는 것은 아닙니다.

관측 가능 조건:

  1. 위치: 관측자의 위도가 해당 별자리를 볼 수 있는 곳인가?
  2. 시기: 유성우가 발생하는 날짜인가?
  3. 달의 위상: 달빛이 너무 밝지 않은가? (가장 중요한 변수)

이번 포스팅에서는 파이썬을 활용해 "내가 있는 도시에서 언제 유성우를 가장 잘 볼 수 있을지" 예측하는 로직을 구현해 보겠습니다.


02. 데이터 준비 및 전처리

사용할 데이터셋은 Microsoft Learning GitHub에서 가져왔습니다.

  • meteorshowers.csv: 5대 유성우 발생 기간 및 정보
  • constellations.csv: 별자리별 관측 가능한 위도 범위
  • moonphases.csv: 날짜별 달의 위상 (밝기)
  • cities.csv: 주요 도시의 위도 정보

1) 누락된 데이터 추가

데이터셋에 가상의 유성우 'Chang'e'(중국 달 탐사선 이름에서 유래한 가상 데이터)와 'Draco' 별자리 정보가 누락되어 있어 임의로 추가해 줍니다.

import pandas as pd
import numpy as np

# 유성우 데이터 추가
change_meteor_shower = {'name':'Chang\\'e','radiant':'Draco', 'bestmonth':'october', 'startmonth':'october', 'startday':1, 'endmonth':'october', 'endday':31, 'hemisphere':'northern', 'preferredhemisphere':'northern'}
meteor_showers = pd.concat([meteor_showers, pd.DataFrame([change_meteor_shower])], ignore_index=True)

# 별자리 데이터 추가
draco_constellation = {'constellation':'Draco','bestmonth':'july', 'latitudestart':90, 'latitudeend':-15, 'besttime':2100, 'hemisphere':'northern'}
constellations = pd.concat([constellations, pd.DataFrame([draco_constellation])], ignore_index=True)

2) 데이터 타입 변환 (매핑)

컴퓨터가 이해하기 쉽도록 문자열 데이터를 숫자로 변환합니다.

  • 월(Month): 영어 이름 숫자 (1~12)
  • 반구(Hemisphere): 북반구/남반구 0, 1 (수치화)
# 월 매핑 딕셔너리
months = {'january':1, 'february':2, 'march':3, 'april':4, 'may':5, 'june':6, 'july':7, 'august':8, 'september':9, 'october':10, 'november':11, 'december':12}

# 매핑 적용
cols_to_map = ['bestmonth', 'startmonth', 'endmonth'] # 유성우 데이터
for col in cols_to_map:
    meteor_showers[col] = meteor_showers[col].map(months)

moon_phases.month = moon_phases.month.map(months)
constellations.bestmonth = constellations.bestmonth.map(months)

# 반구 정보 매핑
hemispheres = {'northern':0, 'southern':1, 'northern, southern':3}
meteor_showers.hemisphere = meteor_showers.hemisphere.map(hemispheres)
constellations.hemisphere = constellations.hemisphere.map(hemispheres)

3) 날짜 데이터 변환 (DateTime)

연산 편의를 위해 월, 일을 합쳐 datetime 객체로 변환합니다. (연도는 임의로 2020년으로 고정)

# YYYYMMDD 형태로 만들어 datetime 변환
meteor_showers['startdate'] = pd.to_datetime(2020*10000+meteor_showers.startmonth*100+meteor_showers.startday, format='%Y%m%d')
meteor_showers['enddate'] = pd.to_datetime(2020*10000+meteor_showers.endmonth*100+meteor_showers.endday, format='%Y%m%d')
moon_phases['date'] = pd.to_datetime(2020*10000+moon_phases.month*100+moon_phases.day, format='%Y%m%d')

4) 불필요한 열 삭제

전처리가 끝났거나 중복되는 컬럼을 삭제하여 데이터프레임을 경량화합니다.

meteor_showers = meteor_showers.drop(['startmonth', 'startday', 'endmonth', 'endday', 'hemisphere'], axis=1)
moon_phases = moon_phases.drop(['month','day','moonphase','specialevent'], axis=1)
constellations = constellations.drop(['besttime'], axis=1)

03. 결측치 처리 (선형 보간법)

moon_phases 데이터를 확인해보면 달의 밝기(percentage)가 매일 기록되지 않고 띄엄띄엄 있습니다.

1일이 0%이고 5일이 100%라면, 그 사이 2~4일은 25%, 50%, 75% 정도로 추정할 수 있습니다. 이를 선형 보간법(Linear Interpolation)이라고 합니다.

방법 1: 반복문을 이용한 수동 구현

non_null_indices = moon_phases[moon_phases['percentage'].notnull()].index.tolist()

for i in range(len(non_null_indices) - 1):
    start_idx = non_null_indices[i]
    end_idx = non_null_indices[i + 1]
    start_val = moon_phases.at[start_idx, 'percentage']
    end_val = moon_phases.at[end_idx, 'percentage']
    
    num_missing = end_idx - start_idx - 1
    
    if num_missing > 0:
        # np.linspace로 시작과 끝 사이를 균등하게 채움
        fill_values = np.linspace(start_val, end_val, num_missing + 2)[1:-1]
        moon_phases.loc[start_idx + 1:end_idx - 1, 'percentage'] = fill_values

방법 2: Pandas 내장 함수 활용
사실 판다스에는 이 기능을 한 줄로 처리해 주는 interpolate() 함수가 있습니다.

moon_phases['percentage'] = moon_phases['percentage'].interpolate(method='linear')

04. 예측 로직 구현

기본 예측 함수

사용자가 도시 이름을 입력하면 다음과 같은 순서로 예측합니다.

  1. 도시의 위도(Latitude) 파악
  2. 해당 위도에서 관측 가능한 별자리 필터링
  3. 해당 별자리 근처에서 발생하는 유성우 찾기
  4. 유성우 발생 기간 중 달이 가장 어두운 날(Best Date) 찾기
def predict_best_meteor_shower_viewing(city):
    meteor_shower_string = ""

    # 1. 도시 확인
    if city not in cities.values:
        return "Unfortunately, " + city + " isn't available."

    # 2. 위도 기반 별자리 필터링
    latitude = cities.loc[cities['city'] == city, 'latitude'].iloc[0]
    constellation_list = constellations.loc[(constellations['latitudestart'] >= latitude) & (constellations['latitudeend'] <= latitude), 'constellation'].tolist()

    if not constellation_list:
        return "Unfortunately, there are no meteor showers viewable from "+ city + "."

    meteor_shower_string = "In " + city + " you can see the following meteor showers:\n"
    
    for constellation in constellation_list:
        # 3. 유성우 매칭
        meteor_shower = meteor_showers.loc[meteor_showers['radiant'] == constellation, 'name'].iloc[0]
        start_date = meteor_showers.loc[meteor_showers['radiant'] == constellation, 'startdate'].iloc[0]
        end_date = meteor_showers.loc[meteor_showers['radiant'] == constellation, 'enddate'].iloc[0]

        # 4. 기간 내 달 위상 확인
        moon_list = moon_phases.loc[(moon_phases['date'] >= start_date) & (moon_phases['date'] <= end_date)]

        if meteor_shower == 'Chang\'e':
            best_date = moon_list.loc[moon_list['percentage'].idxmax()]['date'] # Chang'e는 보름달일 때 관측
        else:
            best_date = moon_list.loc[moon_list['percentage'].idxmin()]['date'] # 일반 유성우는 그믐달일 때 관측

        meteor_shower_string += f"{meteor_shower} is best seen if you look towards the {constellation} constellation on {best_date.strftime('%B %d, %Y')}.\n"
    
    return meteor_shower_string

05. AI 개선 로직: 점수(Score) 기반

기존 로직은 단순히 가능하다/아니면 가장 좋은 날 하루만 알려줍니다. Gemini의 도움을 받아 관측 점수(Visibility Score)를 매겨 랭킹을 보여주는 방식으로 고도화해 보았습니다.

개선 포인트:

  • 속도 향상: pd.merge로 데이터를 미리 병합하여 루프 최소화
  • 점수화 알고리즘:
  • 위도 점수 (): 별자리가 하늘 중앙에 올수록 높은 점수
  • 달 위상 점수 (): 달이 어두울수록 높은 점수
def predict_enhanced_meteor_showers(city_name):
    # 1. 데이터 병합 (미리 합치기)
    df_merged = pd.merge(meteor_showers, constellations, left_on='radiant', right_on='constellation')
    
    city = cities[cities['city'] == city_name]
    if city.empty: return "City not found."
    city_lat = city['latitude'].iloc[0]
    
    # 2. 관측 가능 유성우 필터링
    viewable = df_merged[(df_merged['latitudestart'] >= city_lat) & (df_merged['latitudeend'] <= city_lat)].copy()
    
    results = []
    
    for _, shower in viewable.iterrows():
        # 3. 위도 점수 계산 (중앙에 가까울수록 Good)
        lat_range = shower['latitudestart'] - shower['latitudeend']
        mid_lat = (shower['latitudestart'] + shower['latitudeend']) / 2
        lat_score = max(0, 1 - (abs(city_lat - mid_lat) / (lat_range / 2)))
        
        # 4. 달 위상 점수 계산
        relevant_moons = moon_phases[(moon_phases['date'] >= shower['startdate']) & (moon_phases['date'] <= shower['enddate'])]
        
        for _, moon in relevant_moons.iterrows():
            moon_score = 1 - moon['percentage']
            total_score = (lat_score * 0.5 + moon_score * 0.5) * 100 # 100점 만점 환산
            
            results.append({
                'Meteor Shower': shower['name'],
                'Date': moon['date'].strftime('%Y-%m-%d'),
                'Constellation': shower['radiant'],
                'Score': round(total_score, 2)
            })
            
    return pd.DataFrame(results).sort_values(by='Score', ascending=False)

06. 결과 비교

원본 코드 결과 (Seoul)

단순히 유성우 이름과 가장 좋은 날짜 하루씩만 나열합니다.

In Seoul you can see the following meteor showers:

  • Lyrids is best seen ... on April 22, 2020.
  • Eta Aquarids is best seen ... on April 22, 2020.
  • ...

개선된 코드 결과 (Seoul)

날짜별로 관측 점수가 높은 순서대로 정렬되어, 언제가 '진짜' 최적기인지 알 수 있습니다.

Meteor ShowerDateConstellationScore
Chang'e2020-10-16Draco136.0
Chang'e2020-10-15Draco135.0
............
Eta Aquarids2020-05-05Aquarius22.0

같은 유성우라도 날짜별로 점수가 다르게 계산되어 더 정밀한 관측 계획을 세울 수 있게 되었습니다.


도움이 되셨다면 ❤️와 댓글 부탁드립니다!

profile
제 별명이 감자입니다

0개의 댓글