UFO Sightings Comprehensive Analysis and Visualizations
By Eldar · Published July 9, 2026
Exploratory analysis of 80,000+ UFO sightings worldwide covering temporal trends, geospatial patterns, shapes, durations, and seasonal effects with interactive visualizations.
- eda
- geospatial
- time-series
- data-visualization
- exploratory-analysis
Inside this notebook
# 🛸 UFO Sightings Around the World — Comprehensive Analysis **Dataset:** [UFO Sightings around the World](https://www.kaggle.com/datasets/camnugent/ufo-sightings-around-the-world) (CC0, 80,000+ records) **Source:** Curated from [planetsig/ufo-reports](https://github.com/planetsig/ufo-reports) This notebook performs a thorough exploratory data analysis: temporal trends, geospatial patterns, shape distributions, encounter durations, seasonal effects, and textual description exploration.
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.ticker as mticker
import seaborn as sns
import plotly.express as px
import plotly.graph_objects as go
from plotly.subplots import make_subplots
import warnings
warnings.filterwarnings('ignore')
# Plot styling
plt.rcParams.update({
'figure.facecolor': 'white',
'axes.facecolor': 'white',
'font.size': 12,
'axes.titlesize': 14,
'axes.labelsize': 12,
…Libraries loaded.
# Load the dataset
df = pd.read_csv('ufo_sighting_data.csv')
print(f"Shape: {df.shape[0]:,} rows × {df.shape[1]} columns")
print(f"Memory usage: {df.memory_usage(deep=True).sum() / 1e6:.1f} MB")
df.head(10)Shape: 80,332 rows × 11 columns Memory usage: 57.1 MB
# Schema & data types
dtype_info = pd.DataFrame({
'Column': df.columns,
'Dtype': df.dtypes.values,
'Non-Null': df.notna().sum().values,
'Null%': (df.isna().sum() / len(df) * 100).round(2).values,
'Unique': df.nunique().values,
})
print(dtype_info.to_string(index=False))
print(f"\n--- Sample values per column ---")
for col in df.columns:
print(f"\n{col}: {df[col].dropna().unique()[:5]}")Column Dtype Non-Null Null% Unique
Date_time object 80332 0.00 69586
city object 80332 0.00 19900
state/province object 74535 7.22 67
country object 70662 12.04 5
UFO_shape object 78400 2.41 29
length_of_encounter_seconds object 80332 0.00 705
described_duration_of_encounter object 80332 0.00 8304…# Quick stats and duplicates
print(f"Duplicate rows: {df.duplicated().sum():,}")
print(f"Rows with any null value: {df.isna().any(axis=1).sum():,} ({df.isna().any(axis=1).mean()*100:.1f}%)")
# Column-by-column missing
missing_df = df.isna().sum().to_frame('missing').query('missing > 0').sort_values('missing', ascending=False)
missing_df['pct'] = (missing_df['missing'] / len(df) * 100).round(2)
missing_dfDuplicate rows: 0 Rows with any null value: 13,816 (17.2%)
## Data Cleaning & Preparation
# Convert datetime columns
df['datetime'] = pd.to_datetime(df['Date_time'], format='%m/%d/%Y %H:%M', errors='coerce')
df['date_documented_dt'] = pd.to_datetime(df['date_documented'], format='%m/%d/%Y', errors='coerce')
df['year'] = df['datetime'].dt.year
df['month'] = df['datetime'].dt.month
df['hour'] = df['datetime'].dt.hour
df['day_of_week'] = df['datetime'].dt.dayofweek # 0=Mon, 6=Sun
df['weekday_name'] = df['datetime'].dt.day_name()
# Convert encounter duration to numeric
df['duration_seconds'] = pd.to_numeric(df['length_of_encounter_seconds'], errors='coerce')
df['latitude'] = pd.to_numeric(df['latitude'], errors='coerce')
df['longitude'] = pd.to_numeric(df['longitude'], errors='coerce')
# Drop rows with invalid datetime (critical for time analysis)
invalid_dt = df['datetime'].isna().sum()
print(f"Invalid datetime parsed: {invalid_dt:,} ({invalid_dt/len(df)*100:.2f}%)")
print(f"Invalid duration parsed: {df['duration_seconds'].isna().sum():,} ({df['duration_seconds'].isna().mean()*100:.2f}%)")
…Invalid datetime parsed: 694 (0.86%) Invalid duration parsed: 3 (0.00%) Date range: 1906-11-11 00:00:00 → 2014-05-08 18:45:00 Year range: 1906 → 2014 Duration range: 0s → 97,836,000s
This is a preview. Open the live notebook to see all 38 cells with their charts and full outputs, or fork it into your own Clusy workspace.