%%html
<style>
table.dataframe {
    font-size: 10px;               /* Adjust font size as necessary */
    overflow-x: auto;              /* Adds horizontal scroll to the table */
    width: auto;                   /* Adjust width settings */
    max-width: 100%;               /* Ensures the table does not exceed cell width */
    display: block;
}


/* Container width settings for responsiveness */
@media only screen and (max-width: 768px) {
    .container {
        width: 100% !important;
    }
}
@media only screen and (min-width: 769px) {
    .container {
        width: 80% !important;
    }
}

.container {
    max-width: 1920px !important;
    margin: auto !important;  /* This centers the container */
}

/* Ensuring images are responsive and do not exceed the container's width */
img {
    max-width: 100%;
    height: auto;
}

</style>
%load_ext pretty_jupyter
from plotly.offline import plot
import pandas as pd
from IPython.display import display, HTML
import plotly.express as px
import plotly.graph_objects as go
import plotly.io
import matplotlib.pyplot as plt
from pretty_jupyter.helpers import matplotlib_fig_to_html
import warnings
warnings.filterwarnings('ignore', category=FutureWarning)

The Data of the Dragons

Saying I'm a fan of George R.R. Martin's Fictional World of Ice & Fire is a bit of an understatement. From Fire & Blood and the A Song of Ice and Fire series, to his companion books The World of Ice & Fire and The Rise of the Dragon, I've read it all. So, you can imagine my excitement at the announcement of House of The Dragon, a Game of Thrones prequel to make up for that season 8 dumpster fire. I re-read all of Fire & Blood just to prepare for the series premiere, and while both the book and the show did not disappoint, I had even more questions.

We're given bits and pieces about the dragons, like general age, size, and some of their coloring but doesn't give much else. How are the dragons connected to each other? With the end of season 2, and more dragons having been introduced, I've decided that I have no other choice but to get my books out again and create a project that focuses on the dragons and their relationships.

For quick reference or just a general overview of the dragons themselves, below are all the dragons featured in the dataset. Clicking on a specific dragon will display a pop-up with a short description.

Please note that all dragon images were created by me using a personally trained Stable Diffusion 3.0 model. These images are for viewing only and are not available for personal or public use, sharing, or distribution.


The History of Dragons in Westeros

Background and Origins

The Valyrians discovered dragons in a ring of volcanoes known as the Fourteen Flames. They mastered the art of raising and riding dragons, which helped them build the powerful Valyrian Freehold Empire. The Fourteen flames erupted, wiping out most of the dragons and dragonlords in Valyria in what became known as the Doom of Valyria. However, House Targaryen survived, having left for Dragonstone a few years prior with their dragons—Balerion, Vhagar, and Meraxes.

Meraxes_Balerion_Vhagar

The First Three Dragons in Westeros: Meraxes (left), Balerion (center), and Vhagar (right)

The Dance of the Dragons

The Dance of the Dragons was a civil war within the Targaryen family that began after King Viserys I died. A power struggle broke out over who should sit on the Iron Throne. His daughter, Rhaenyra, had been named his heir, but her younger half-brother, Aegon II, usurped the throne. The resulting conflict led to the death of many Targaryens and their dragons. The war devastated the realm, weakened the Targaryen's claim to the Iron Throne, and led to the eventual decline of dragons in Westeros.

At the time of the civil war, there were twenty dragons. The largest were Vhagar, Cannibal, Vermithor, Silverwing, Dreamfyre, Meleys, Caraxes, Syrax, and Seasmoke. Others like Sunfyre, Vermax, Tessarion, and Moondancer weren't as large but still battle-ready. The wild dragons, Grey Ghost, Sheepstealer, and the Cannibal lived on Dragonstone but had no riders.

Wild Dragons

The Wild Dragons: Grey Ghost (left), Sheepstealer (center), and Cannibal (right)

Reproduction and Gender

There’s an ongoing debate about how dragons reproduce. Some believe dragons can change sex, while others argue that their gender is fixed. For this project, we assume dragons require mates to lay eggs, based on direct quotes from George R. R. Martin himself. As he wrote in The Rise of The Dragon:

'Over time, Princess Rhaenyra became close friends with her good-sister, Lady Laena, and spent more and more time with both Laena and Prince Daemon, flying to and fro between Driftmark and Dragonstone. During this period, Rhaenyra's dragon Syrax laid several clutches of eggs, doubtless the result of matings with Caraxes.'

-The Rise of the Dragon, p. 193

Syrax_Caraxes

Syrax (left) and Caraxes (right)


Introduction

Fire & Blood is a partial history of the Targaryens—a family of dragonriders that ruled for three centuries over all of Westeros. House of the Dragon is an HBO series that focuses on a very short but very significant time period during their reign, between 129-131 AC, known as The Dance of The Dragons. The Dance was a civil war between the Targaryens and was the beginning of the end for the dragons in Westeros and 300 years of Targaryen rule. My goal with this project is to give audiences of the show who might not have read Fire & Blood more insight into the dragons themselves.

Project Overview

If there were no dragons in Westeros before Aegon the Conqueror, then all the dragons alive at the time of The Dance must be related.

We know some dragons are She-Dragons based on their ability to lay eggs. Can we use their known traits to determine paternity or predict the characteristics of younger dragons without canon descriptions?

This project examines the lineages of the dragons involved in The Dance of The Dragons in George R. R. Martin's Fire & Blood, as featured on HBO's House of the Dragon. The goal is to map out the familial connections, create a dragon family tree, and predict the characteristics of dragons without canon descriptions.

Methodology

  1. Data Loading and Preparation:
    • Load the dataset and clean it to handle missing or inconsistent values.
  2. Exploratory Dragon Analysis (EDA):
    • Explore the data.
    • Validate or correct all maternal relationships.
  3. The Genetics of Dragons:
    • Determine recessive and dominant hereditary traits.
  4. The Paternity of Dragons:
    • Use trait dominance information and machine learning, in combination with canon material, to assign dragon paternity.
  5. The Prediction of Dragons:
    • Use lineages and dominance information to predict the physical appearances of all dragons without descriptions in the books.
    • Generate accurate and original images of the dragons using the predicted physical appearances.
  6. The Tree of the Dragon:
    • Create a final dragon family tree.

Cleaning and Preparation

Taking an initial look at the dataset to see any cleaning and reorganizing that might need to be done.

Setup

# Data Exploration and Machine Learning Libraries:
import pandas as pd
import numpy as np
import ast 
import re
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import RandomForestClassifier
from sklearn.preprocessing import LabelEncoder
from collections import Counter
import colorsys
from keras.models import Sequential
from keras.layers import Dense, Input

# Visualization Libraries
import matplotlib.pyplot as plt
import seaborn as sns
import networkx as nx
from networkx.drawing.nx_agraph import graphviz_layout
import matplotlib.lines as mlines
from matplotlib import patches
from matplotlib.patches import Rectangle

Load the Dataset

# Load the dataset
dragon_data = pd.read_csv('dragon_data.csv')
# Data Exploration and Machine Learning Libraries:
import pandas as pd
import numpy as np
import ast 
import re
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import RandomForestClassifier
from sklearn.preprocessing import LabelEncoder
from collections import Counter
import colorsys
from keras.models import Sequential
from keras.layers import Dense, Input

# Visualization Libraries
import matplotlib.pyplot as plt
import seaborn as sns
import networkx as nx
from networkx.drawing.nx_agraph import graphviz_layout
import matplotlib.lines as mlines
from matplotlib.patches import Rectangle

# Defining the color scheme

# Reds color palette
red_palette = ['#001514', '#1B1110', '#290F0E', '#360D0C', '#440B0A', '#4B0A09', '#510908', '#5E0706', '#650605', '#6B0504']
sns.set_palette(sns.color_palette(red_palette))

# Green color palette
green_palette = ['#031911', '#042117', '#05291C', '#063122', '#073928', '#09422D', '#0A4A33', '#0B5239', '#0C5A3E', '#0D6244']
sns.set_palette(sns.color_palette(green_palette))

# General color palette
general_palette = ['#092217', '#1B1110', '#063122', '#360D0C', '#001514', '#09422D', '#2D483D', '#510908', '#650605']
sns.set_palette(sns.color_palette(general_palette))

# Smaller color palette
small_palette = ['#28251D', '#1B1110', '#001514', '#2D483D', '#510908']
sns.set_palette(sns.color_palette(small_palette))

# Blacks vs. Green palette
allegiance_palette = ['#28251D', '#092818']

# Set the maximum width of each column
pd.set_option('display.max_colwidth', 30) 
# Load the dataset
dragon_data = pd.read_csv('dragon_data.csv')

Dataset Information

First Few Rows

dragon_data.head(5)
Name Gender BirthDate DeathDate HatchedFrom FirstRider LastRider Riders NotableEvents Size ... WingMembraneColor EyeColor PersonalityTraits FoughtInTheDance DanceAllegiance SurvivedTheDance BattlesDuringTheDance Outcome SurvivedTheBattle KilledBy
0 Arrax Male 115 AC 129 AC Syrax Prince Lucerys Velaryon Prince Lucerys Velaryon ['Prince Lucerys Velaryon'] ['Fight Above Shipbreaker ... Small ... Gold Gold ['Young', 'Strong', 'Agile... Yes Black No Fight Above Shipbreaker Bay Defeat No Vhagar
1 Balerion Male 114 BC 94 AC Unknown King Aegon I Targaryen King Viserys I Targaryen ['King Aegon I Targaryen',... ['Conquest of Westeros', '... Huge ... Black Red ['Powerful', 'Willful', 'F... No NaN NaN NaN NaN NaN NaN
2 Caraxes Male 50 AC 130 AC Dreamfyre Prince Aemon Targaryen Prince Daemon Targaryen ['Prince Aemon Targaryen',... ['Battle of the Stepstones... Huge ... Red Yellow ['Formidable', 'Fearsome',... Yes Black No Assault on Harrenhal, Batt... Victory, Victory, Victory,... Yes, Yes, Yes, No N/A, N/A, N/A, Vhagar, Sun...
3 Dreamfyre Female 32 AC 130 AC Meraxes Queen Rhaena Targaryen Queen Helaena Targaryen ['Queen Rhaena Targaryen',... ['Storming of the Dragonpit'] Large ... Silver Pale Blue ['Loyal', 'Formidable', 'P... Yes Green No Storming of the Dragonpit Defeat No Humans
4 Grey Ghost Male 90 AC 130 AC Silverwing NaN NaN [] ['Fight Above Dragonmount'] Medium ... Pale Grey-White Bright Green ['Wild', ' Aggressive', 'S... Yes '' No Fall of Dragonstone Defeat No Sunfyre

5 rows × 21 columns

Column Descriptions

Column Description
Name Name of the dragon
Gender Gender of the dragon
Birthdate Birthdate of the dragon (BC = Before Conquest, AC = After Conquest)
Deathdate Deathdate of the dragon
HatchedFrom Source dragon egg
FirstRider First bonded Targaryen dragonrider
LastRider Last bonded Targaryen dragonrider
Riders List of all known, bonded, Targaryen dragonriders
NotableEvents Key events involving the dragon
Size Size of the dragon (Small, Medium, Large, Huge)
ScaleColor Color of the dragon's scales
WingMembraneColor Color of the dragon's wing membranes, accents, or crests
EyeColor Color of the dragon's eyes
PersonalityTraits Traits and characteristics of the dragon
FoughtInTheDance Whether the dragon fought in the Dance of the Dragons
DanceAllegiance Allegiance of the dragonrider during the Dance of the Dragons
SurvivedTheDance Whether the dragon survived the Dance of the Dragons
BattlesDuringTheDance Specific battles the dragon participated in during the Dance
Outcome Outcome of the dragon's participation in battles
SurvivedTheBattle Whether the dragon survived each battle
KilledBy Cause of dragon's death

Structure

dragon_data.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 24 entries, 0 to 23
Data columns (total 21 columns):
 #   Column                 Non-Null Count  Dtype 
---  ------                 --------------  ----- 
 0   Name                   24 non-null     object
 1   Gender                 24 non-null     object
 2   BirthDate              24 non-null     object
 3   DeathDate              24 non-null     object
 4   HatchedFrom            24 non-null     object
 5   FirstRider             22 non-null     object
 6   LastRider              22 non-null     object
 7   Riders                 23 non-null     object
 8   NotableEvents          23 non-null     object
 9   Size                   24 non-null     object
 10  ScaleColor             24 non-null     object
 11  WingMembraneColor      24 non-null     object
 12  EyeColor               24 non-null     object
 13  PersonalityTraits      24 non-null     object
 14  FoughtInTheDance       24 non-null     object
 15  DanceAllegiance        20 non-null     object
 16  SurvivedTheDance       20 non-null     object
 17  BattlesDuringTheDance  20 non-null     object
 18  Outcome                20 non-null     object
 19  SurvivedTheBattle      20 non-null     object
 20  KilledBy               19 non-null     object
dtypes: object(21)
memory usage: 4.1+ KB

Missing Values

missing_values = dragon_data.isnull().sum()
missing_values
Name                     0
Gender                   0
BirthDate                0
DeathDate                0
HatchedFrom              0
FirstRider               2
LastRider                2
Riders                   1
NotableEvents            1
Size                     0
ScaleColor               0
WingMembraneColor        0
EyeColor                 0
PersonalityTraits        0
FoughtInTheDance         0
DanceAllegiance          4
SurvivedTheDance         4
BattlesDuringTheDance    4
Outcome                  4
SurvivedTheBattle        4
KilledBy                 5
dtype: int64
# Handling Missing Values
dragon_data['FirstRider'].fillna('Unknown', inplace=True)
dragon_data['Riders'].fillna('Unknown', inplace=True)
dragon_data['NotableEvents'].fillna('Unknown', inplace=True)
dragon_data['LastRider'].fillna('Unknown', inplace=True)
dragon_data['DanceAllegiance'].fillna('Unknown', inplace=True)
dragon_data['SurvivedTheDance'].fillna('Unknown', inplace=True)
dragon_data['BattlesDuringTheDance'].fillna('Unknown', inplace=True)
dragon_data['Outcome'].fillna('Unknown', inplace=True)
dragon_data['SurvivedTheBattle'].fillna('Unknown', inplace=True)
dragon_data['KilledBy'].fillna('Unknown', inplace=True)
dragon_data['DeathDate'].fillna('Unknown', inplace=True)

# Focus on relevant columns
relevant_columns = ['Name', 'HatchedFrom', 'Gender', 'BirthDate', 'DeathDate', 'Size', 'ScaleColor', 'WingMembraneColor', 'EyeColor', 'PersonalityTraits', 'FoughtInTheDance', 'DanceAllegiance', 'SurvivedTheDance', 'BattlesDuringTheDance', 'Outcome', 'SurvivedTheBattle', 'KilledBy']
cleaned_dragon_data = dragon_data[relevant_columns]
cleaned_dragon_data.to_csv('cleaned_dragon_data.csv', index=False)

# Saving cleaned data
dragon_data = pd.read_csv('cleaned_dragon_data.csv')

# Extract unique values for 'ScaleColor', 'WingMembraneColor', and 'EyeColor'
scale_colors = dragon_data['ScaleColor'].unique()
wing_colors = dragon_data['WingMembraneColor'].unique()
eye_colors = dragon_data['EyeColor'].unique()

# Data conversion to lists
dragon_data['PersonalityTraits'] = dragon_data['PersonalityTraits'].apply(lambda x: x.strip("[]").replace("'", "").split(', ') if isinstance(x, str) else [])

# Flatten the list of lists to extract unique values properly
flattened_traits = [trait for sublist in dragon_data['PersonalityTraits'] for trait in sublist]

# Sorting unique values
traits_list = sorted(set(flattened_traits))
scale_colors_list = sorted(scale_colors)
wing_colors_list = sorted(wing_colors)
eye_colors_list = sorted(eye_colors)
# Define scale colors
defined_scale_colors = {
  'Black': 'Black',
  'Bronze': 'Bronze',
  'Brown': 'Bronze',
  'Charcoal': 'Black',
  'Dark Cobalt Blue': 'Blue',
  'Dark Grey': 'Black',
  'Dark Grey-Blue': 'Blue',
  'Gold': 'Gold',
  'Jade Green': 'Green',
  'Olive Green': 'Green',
  'Pale Blue': 'Blue',
  'Pale Green': 'Green',
  'Pale Grey-White': 'White',
  'Pale Pink': 'Pink',
  'Pale Silver-White': 'White',
  'Pale Silver-Grey': 'Silver',
  'Pale White': 'White',
  'Pearl-White': 'White',
  'Red': 'Red',
  'Scarlet Red': 'Red',
  'Silver': 'Silver',
  'Yellow': 'Yellow',
}

defined_wing_colors = {    
  'Black': 'Black',
  'Brown': 'Bronze',
  'Copper': 'Copper',
  'Dark Grey': 'Grey', 
  'Gold': 'Gold', 
  'Green-Blue': 'Green',
  'Pale Grey': 'Grey',
  'Pale Grey-White': 'White',
  'Pale Orange': 'Yellow', 
  'Pale Pink': 'Pink',
  'Pearl-White': 'White',
  'Red': 'Red', 
  'Silver': 'Silver', 
  'Tan': 'Bronze',
  'White': 'White',
  'Yellow': 'Yellow'
}

defined_eye_colors = {
  'Bright Blue': 'Blue',
  'Bright Green': 'Green',
  'Copper': 'Bronze',
  'Dark Green': 'Green',
  'Gold': 'Gold',
  'Green': 'Green',
  'Pale Blue': 'Blue',
  'Pale Green': 'Green',
  'Red': 'Red',
  'Yellow': 'Yellow'
}

defined_traits = {    
    ' Aggressive': 'Aggressive',
    'Ferocious': 'Aggressive',
    'Fierce': 'Aggressive',
    'Vicious': 'Aggressive',
    'Gentle': 'Friendly',
    'Powerful': 'Formidable',
    'Strong': 'Formidable',
    'beautiful': 'Beautiful',
    'Brave': 'Willful',
    'Agile': 'Swift',
    'Protective': 'Loyal',
    'formidable': 'Formidable',
}

# Function to map colors and traits
def standardize_scale_color(color):
    return defined_scale_colors.get(color, color)
def standardize_wing_color(color):
    return defined_wing_colors.get(color, color)
def standardize_eye_color(color):
    return defined_eye_colors.get(color, color)
def standardize_traits(traits):
    return [defined_traits.get(trait.strip(), trait.strip()) for trait in traits]

# Function to remove duplicates
def remove_duplicates(traits):
    return list(set(traits))

# Create new columns for standardized colors
dragon_data['StandardizedScaleColor'] = dragon_data['ScaleColor'].apply(standardize_scale_color)
dragon_data['StandardizedWingColor'] = dragon_data['WingMembraneColor'].apply(standardize_wing_color)
dragon_data['StandardizedEyeColor'] = dragon_data['EyeColor'].apply(standardize_eye_color)

# Apply the function to each list in the 'PersonalityTraits' column
dragon_data['StandardizedPersonalityTraits'] = dragon_data['PersonalityTraits'].apply(standardize_traits)

# Remove duplicates in the 'StandardizedPersonalityTraits' column and clean up any characters
dragon_data['StandardizedPersonalityTraits'] = dragon_data['StandardizedPersonalityTraits'].apply(remove_duplicates)
dragon_data['StandardizedPersonalityTraits'] = dragon_data['StandardizedPersonalityTraits'].apply(lambda traits: [trait.strip("[]").replace("'", "") for trait in traits] if isinstance(traits, list) else [])

# Extract unique values for 'ScaleColor', 'WingMembraneColor', and 'EyeColor'
standardizedscale_colors = dragon_data['StandardizedScaleColor'].unique()
standardizedwing_colors = dragon_data['StandardizedWingColor'].unique()
standardizedeye_colors = dragon_data['StandardizedEyeColor'].unique()

# Flatten the list of lists to extract unique values properly
standardizedflattened_traits = [trait for sublist in dragon_data['StandardizedPersonalityTraits'] for trait in sublist]

# Sorting unique values
standardizedtraits_list = sorted(set(standardizedflattened_traits))
standardizedscale_colors_list = sorted(standardizedscale_colors)
standardizedwing_colors_list = sorted(standardizedwing_colors)
standardizedeye_colors_list = sorted(eye_colors)

dragon_data.to_csv('final_cleaned_dragon_data.csv', index = False)
def list_to_string(list_items):
    """Convert list to a comma-separated string."""
    return ', '.join(map(str, list_items))

# Converting the original lists of unique values to comma-separated strings
original_traits_str = list_to_string(traits_list)
original_scale_colors_str = list_to_string(scale_colors_list)
original_wing_colors_str = list_to_string(wing_colors_list)
original_eye_colors_str = list_to_string(eye_colors_list)

# Converting the standardized lists to comma-separated strings
standardized_traits_str = list_to_string(standardizedtraits_list)
standardized_scale_colors_str = list_to_string(standardizedscale_colors_list)
standardized_wing_colors_str = list_to_string(standardizedwing_colors_list)
standardized_eye_colors_str = list_to_string(standardizedeye_colors_list)

Cleaning & Data Preparation

Cleaning:

  • Fill missing values with 'Unknown' for categorical columns and 'None' for list-like structures
  • Filter our data to include only the columns relevant to analyzing dragon lineages, physical characteristics, and behavioral traits.
    • Our relevant columns in this case will be: Name,HatchedFrom, Gender, BirthDate, Deathdate, Size, ScaleColor, WingMembraneColor, EyeColor, PersonalityTraits, FoughtInTheDance, DanceAllegiance, SurvivedTheDance, BattlesDuringTheDance, Outcome, SurvivedTheBattle, KilledBy
      # Focus on relevant columns
      relevant_columns = ['Name', 'HatchedFrom', 'Gender', 'BirthDate', 'Deathdate', 'Size', 'ScaleColor', 'WingMembraneColor', 'EyeColor', 'PersonalityTraits', 'FoughtInTheDance', 'DanceAllegiance', 'SurvivedTheDance', 'BattlesDuringTheDance', 'Outcome', 'SurvivedTheBattle', 'KilledBy']
      cleaned_dragon_data = dragon_data[relevant_columns]
      

Preparation:

In this step, we'll make sure the physical traits and behavioral traits are standardized to make it easier to analyze and compare the dragons. We'll list unique values from ScaleColor, WingMembraneColor, EyeColor, and PersonalityTraits, then create a function that groups each unique trait into more generalized group.

  • Standardize colors for easier grouping
  • Convert rows with multiple entries like ['Young', 'Strong', 'Agile', 'Brave'] into lists.
# Extract unique values for 'ScaleColor', 'WingMembraneColor', and 'EyeColor'
scale_colors = dragon_data['ScaleColor'].unique()
wing_colors = dragon_data['WingMembraneColor'].unique()
eye_colors = dragon_data['EyeColor'].unique()

# Data conversion to lists
dragon_data['PersonalityTraits'] = dragon_data['PersonalityTraits'].apply(lambda x x.strip("[]").replace("'", "").split(', ') if isinstance(x, str) else [])

# Function to map colors and traits
def standardize_scale_color(color):
    return defined_scale_colors.get(color, color)
def standardize_wing_color(color):
    return defined_wing_colors.get(color, color)
def standardize_eye_color(color):
    return defined_eye_colors.get(color, color)
def standardize_traits(traits):
    return [defined_traits.get(trait.strip(), trait.strip()) for trait in traits]

Original Traits

Personality Traits Aggressive, Aggressive, Agile, Beautiful, Brave, Cunning, Fearsome, Ferocious, Fierce, Formidable, Friendly, Gentle, Heavy, Huge, Lazy, Loyal, Moody, Old, Powerful, Protective, Proud, Shy, Slender, Strong, Swift, Vicious, Wild, Willful, Young, beautiful, formidable
Scale Colors Black, Bronze, Brown, Charcoal, Dark Cobalt Blue, Dark Grey, Dark Grey-Blue, Gold, Jade Green, Olive Green, Pale Blue, Pale Green, Pale Grey-White, Pale Pink, Pale Silver-Grey, Pale Silver-White, Pearl-White, Red, Scarlet Red, Silver, Yellow
Wing & Accent Colors Black, Brown, Copper, Dark Grey, Gold, Green-Blue, Pale Grey-White, Pale Orange, Pale Pink, Pearl-White, Red, Silver, Tan, Yellow
Eye Colors Bright Blue, Bright Green, Copper, Gold, Green, Pale Blue, Pale Green, Red, Yellow

Standardized Traits

Standardized Personality Traits Aggressive, Beautiful, Cunning, Fearsome, Formidable, Friendly, Heavy, Huge, Lazy, Loyal, Moody, Old, Proud, Shy, Slender, Swift, Wild, Willful, Young
Standardized Scale Colors Black, Blue, Bronze, Gold, Green, Pink, Red, Silver, White, Yellow
Standardized Wing & Accent Colors Black, Bronze, Copper, Gold, Green, Grey, Pink, Red, Silver, White, Yellow
Standardized Eye Colors Bright Blue, Bright Green, Copper, Gold, Green, Pale Blue, Pale Green, Red, Yellow
# Define the function to convert dates to numeric values
def convert_to_numeric_date(date_str):
    if pd.isna(date_str):
        return None
    if 'BC' in date_str:
        return -int(re.findall(r'\d+', date_str)[0])
    elif 'AC' in date_str:
        return int(re.findall(r'\d+', date_str)[0])
    return None

# Convert BirthDate and DeathDate to numeric values
dragon_data['BirthDate_numeric'] = dragon_data['BirthDate'].apply(convert_to_numeric_date)
dragon_data['DeathDate_numeric'] = dragon_data['DeathDate'].apply(convert_to_numeric_date)

# Filter only female dragons
female_dragons = dragon_data[dragon_data['Gender'] == 'Female']

# Define function 
def find_potential_mothers_corrected(dragon_name, birth_date, female_dragons):
    potential_mothers = []
    for _, row in female_dragons.iterrows():
        if row['Name'] != dragon_name and row['BirthDate_numeric'] < birth_date < row['DeathDate_numeric']:
            potential_mothers.append(row['Name'])
    return potential_mothers

#Apply the function to each dragon
dragon_data['PotentialMothers'] = dragon_data.apply(lambda row: find_potential_mothers_corrected(row['Name'], row['BirthDate_numeric'], female_dragons), axis=1)

potential_mothers = dragon_data[['Name', 'PotentialMothers']]

def format_potential_mothers_vertical(df):
    markdown_output = ""
    for _, row in df.iterrows():
        # Check if the list is non-empty and does not contain 'None'
        if row['PotentialMothers'] and 'None' not in row['PotentialMothers']:
            mothers_list = ', '.join(row['PotentialMothers'])
            markdown_output += f"**{row['Name']}**: {mothers_list}\n\n"
        else:
            continue  # Skip adding to markdown_output if mothers are 'None'
    return markdown_output

# Apply the function to the dataframe
potential_mothers_markdown = format_potential_mothers_vertical(potential_mothers)

Cleaning & Preparation Summary:

  1. Missing values in categorical columns were filled with 'Unknown'.
  2. The dataset was filtered to include only the columns relevant to analyzing dragon lineages, physical characteristics, and behavioral traits.
  3. Created dictionaries to map similar scale and wing colors to create new columns StandardizedScaleColor, StandardizedEyeColor, and StandardizedWingColor.
  4. Converted the PersonalityTraits column to lists, flattened the list of lists to identify unique traits, mapped the unique traits to create a new column StandardizedPersonalityTraits.
  5. Saved the cleaned and standardized dataset as final_cleaned_dragon_data.csv

Exploratory Dragon Analysis

EDA will involve establishing any patterns and connections in order to validate or correct all current maternal relationships in the HatchedFrom column for further analysis.

Establishing the Mothers of the Dragons

While there's a HatchedFrom column already, those are just estimates based on various source materials. I think it's important we use the estimated birthdates and deathdates, as well as knowledge of the connections between their dragonriders, to make as accurate a family tree as possible. All mothers from the HatchedFrom column are: Meraxes, Vhagar, Quicksilver, Dreamfyre, Silverwing, Meleys, Syrax, & Tessarion

Mother Dragons

Top Row: Meraxes, Vhagar, Quicksilver, and Dreamfyre
Bottom Row: Silverwing, Meleys, Syrax, and Tessarion

To determine the most likely mother dragon for each dragon, we will:

  1. Filter Female Dragons
    # Filter only female dragons
    female_dragons = dragon_data[dragon_data['Gender'] == 'Female']
    
  2. Check Life Span Overlap
    # Define function 
    def find_potential_mothers_corrected(dragon_name, birth_date, female_dragons):
     potential_mothers = []
     for _, row in female_dragons.iterrows():
         if row['Name'] != dragon_name and row['BirthDate_numeric'] < birth_date < row['DeathDate_numeric']:
             potential_mothers.append(row['Name'])
     return potential_mothers
    
  3. Compare each offspring's birth date with the lifespan of all female dragons
    #Apply the function to each dragon
    dragon_data['PotentialMothers'] = dragon_data.apply(lambda row: find_potential_mothers_corrected(row['Name'], row['BirthDate_numeric'], female_dragons), axis=1)
    

Dragons & Their Possible Mothers

Arrax: Dreamfyre, Meleys, Syrax, Tessarion, Vhagar

Caraxes: Dreamfyre, Vhagar

Dreamfyre: Quicksilver, Vhagar

Grey Ghost: Dreamfyre, Meleys, Vhagar

Meleys: Dreamfyre, Vhagar

Moondancer: Dreamfyre, Meleys, Syrax, Tessarion, Vhagar

Morghul: Dreamfyre, Meleys, Moondancer, Syrax, Tessarion, Vhagar

Morning: Dreamfyre, Moondancer, Shrykos, Syrax, Tessarion, Vhagar

Quicksilver: Meraxes, Vhagar

Seasmoke: Dreamfyre, Meleys, Vhagar

Sheepstealer: Dreamfyre, Quicksilver, Vhagar

Shrykos: Dreamfyre, Meleys, Moondancer, Syrax, Tessarion, Vhagar

Silverwing: Dreamfyre, Quicksilver, Vhagar

Stormcloud: Dreamfyre, Meleys, Moondancer, Shrykos, Syrax, Tessarion, Vhagar

Sunfyre: Dreamfyre, Meleys, Syrax, Vhagar

Syrax: Dreamfyre, Meleys, Vhagar

Tessarion: Dreamfyre, Meleys, Syrax, Vhagar

Tyraxes: Dreamfyre, Meleys, Syrax, Tessarion, Vhagar

Vermax: Dreamfyre, Meleys, Syrax, Tessarion, Vhagar

Vermithor: Dreamfyre, Quicksilver, Vhagar

Vhagar: Meraxes

Cannibal: Quicksilver, Vhagar

dragon_data['BirthDate_numeric'] = dragon_data['BirthDate'].apply(convert_to_numeric_date)
dragon_data['DeathDate_numeric'] = dragon_data['DeathDate'].apply(convert_to_numeric_date)

# Plotting the timelines
plt.figure(figsize=(15, 6))
for index, row in dragon_data.iterrows():
    plt.plot([row['BirthDate_numeric'], row['DeathDate_numeric']], [index, index], marker='o')

# Adding labels
plt.yticks(range(len(dragon_data)), dragon_data['Name'])
plt.xlabel('Year')
plt.ylabel('Dragon')
plt.title('Timeline of Dragon Lifespans')
plt.grid(True)
plt.show()

Above is the timeline of dragon lifespans. Each line shows a dragon's birth and death dates on the x-axis. Negative values are Before Conquest and positive values are After Conquest.

We'll use the potential mothers dataframe, this timeline, and canon material to understand the relationships:

  1. Quicksilver: Quicksilver, a She-Dragon, shares pale-silvery tones with Meraxes, likely making Meraxes Quicksilver's mother. Since Meraxes died in 10 AC and dragon eggs rarely stay dormant for over 20 years, Quicksilver is most likely the mother of Dreamfyre and Silverwing.

  2. Vermithor: Born around the same time as Dreamfyre and Silverwing but lacks their pale coloring or their more friendly temperaments. Vermithor is likely Vhagar's offspring due to his size and similar traits.

  3. Cannibal and Sheepstealer: Dreamfyre, Quicksilver, and Vhagar are their potential mothers. They are wild dragons known for their aggression, like Vhagar. Sheepstealer has mud-brown coloring and copper eyes, similar to Vermithor, while Cannibal is pitch black, like Balerion. From color and temperament, Vhagar is most likely their mother.

  4. Dreamfyre: Bonded to Queen Rhaena, Dreamfyre laid eggs by 43 AC, including a clutch in 50 AC from which Meleys and Caraxes may have hatched.

    "A clutch of three eggs was laid by Dreamfyre on Fair Isle around 50 AC, which hatched after being brought to Dragonstone."

    -A Wiki of Ice & Fire

  5. Tessarion: Tessarion has blue coloring and was a cradle egg (a dragon egg that is placed in the cradle of a newborn Targaryen baby) for Prince Daeron. After King Viserys I’s death, the Hightowers (Team Green) couldn’t access Dragonstone, making Dreamfyre the most likely mother due to her presence in King's Landing and blue coloring.

    "Later in 114, Alicent gave birth to Daeron. Viserys couldn't admit that he only put eggs in the cradles of Rhaenyra’s sons because he doubted their paternity; he put an egg in his son Daeron’s cradle as well; this egg hatched into Tessarion."

    -A Wiki of Ice & Fire

  6. Seasmoke: Meleys is likely Seasmoke's mother, as Rhaenys probably gave her son a dragon egg from her own dragon.

  7. Silverwing: Silverwing is likely Grey Ghost's mother due to her time on Dragonstone, her temperament, and similar physical traits. She may also be the mother of Syrax and Sunfyre.

  8. Shrykos and Morghul: These dragons were cradle eggs the twin children of King Aegon II Targaryen and Queen Helaena Targaryen, born in 123 AC. Vermithor resided in Dragonstone by 103 AC, while Dreamfyre was in the Dragonpit at King's Landing with Princess Helaena. Since Vermithor and Dreamfyre couldn't have mated due to being in separate locations, and the Greens had no access to Dragonstone, Tessarion is the likely mother. She was in King's Landing by 123 AC, and her title "The Blue Queen" indicates she laid eggs, making her the best candidate for the mother of Shrykos and Morghul.

I have most of these entries already placed correctly in the HatchedFrom column but need to update the mother from Meraxes to Quicksilver for Dreamfyre and Silverwing.

# Update the 'HatchedFrom' column for Dreamfyre and Silverwing
dragon_data.loc[dragon_data['Name'].isin(['Dreamfyre', 'Silverwing']), 'HatchedFrom'] = 'Quicksilver'

She-Dragons & Their Offspring

# Update the 'HatchedFrom' column for Dreamfyre and Silverwing
dragon_data.loc[dragon_data['Name'].isin(['Dreamfyre', 'Silverwing']), 'HatchedFrom'] = 'Quicksilver'

dragon_data.to_csv('final_cleaned_dragon_data.csv', index = False)
# Load data
dragon_data = pd.read_csv('final_cleaned_dragon_data.csv')

# Filter out 'Unknown' nodes
dragon_data = dragon_data[dragon_data['HatchedFrom'] != 'Unknown']


# Calculate the number of offspring for each parent
offspring_counts = dragon_data['HatchedFrom'].value_counts()

# Make a directed graph
G = nx.DiGraph()

# Add edges based on parent-offspring relationships in the original dataset
for _, row in dragon_data.iterrows():
    parent = row['HatchedFrom']
    offspring = row['Name']
    G.add_edge(parent, offspring)

# Define node sizes and colors based on dragon name
node_sizes = []
node_colors = []

# Define a color map for the nodes
color_data = {
    'Name': ['Arrax', 'Caraxes', 'Cannibal', 'Dreamfyre', 'Meleys', 'Moondancer', 'Morghul', 'Morning', 'Seasmoke', 'Sheepstealer', 'Shrykos', 'Silverwing', 
             'Stormcloud', 'Sunfyre', 'Syrax', 'Tessarion', 'Tyraxes', 'Vermax', 'Vermithor', 'Vhagar', 'Balerion', 'Meraxes', 'Quicksilver', 'Grey Ghost'],
    'HexCode_Scales': ['#F4EFE6', '#C50016', '#181818', '#7A95C8', '#B22A30', '#8A9C74', '#2F2F2F', '#CC9E9B', '#DADADA', '#4A3931', '#245550', '#bcc6cc', '#354669', 
                       '#E5B347', '#F5D21E', '#2F37A8', '#181818', '#929151', '#A77F5B', '#9A835A', '#181818', '#D3D4DB', '#F3ECEA', '#F2F2F2']
}

color_map = dict(zip(color_data['Name'], color_data['HexCode_Scales']))

mother_node_size = 1500
offspring_node_size = 1000

for node in G.nodes():
    if node in offspring_counts:
        node_sizes.append(mother_node_size)  # Distinct size for mothers
        node_colors.append(color_map.get(node, '#611010'))  # Use color from the map or a default color
    else:
        node_sizes.append(offspring_node_size)  # Distinct size for offspring
        node_colors.append(color_map.get(node, '#123524'))  # Use color from the map or a default color

# Use an automatic layout to get initial positions
pos = nx.spring_layout(G, seed=42)



pos['Vermax'][1] += 0.9
pos['Vermax'][0] += 1.0

pos['Morning'][1] += .9
pos['Morning'][0] += .5

pos['Sheepstealer'][1] += 1.7
pos['Sheepstealer'][0] -= .9

pos['Tessarion'][1] -= 0.4
pos['Tessarion'][0] -= 0.6

pos['Shrykos'][1] -= 1.8
pos['Shrykos'][0] -= 0.5

pos['Morghul'][0] -= 0.7

pos['Vhagar'][1] += 0.2
pos['Vhagar'][0] -= 0.2

pos['Meraxes'][1] -= 0.3

pos['Grey Ghost'][0] -= 0.3
pos['Grey Ghost'][1] -= 0.4

pos['Sunfyre'][0] += 0.3
pos['Sunfyre'][1] -= 0.1

pos['Syrax'][0] += 0.2
pos['Syrax'][1] += 0.1

pos['Stormcloud'][0] -= 0.4

pos['Tyraxes'][0] -= 0.3
pos['Tyraxes'][1] -= 0.2

pos['Meraxes'][1] -= 0.1

pos['Arrax'][1] += 0.2
pos['Arrax'][0] -= 0.2

pos['Moondancer'][0] += 0.1

pos['Seasmoke'][1] -= 0.5

pos['Dreamfyre'][0] += 0.1

# Draw the graph with initial and manually adjusted positions
plt.figure(figsize=(12, 8))

# Draw edges with specified colors
edges = G.edges(data=True)
edge_colors = ['#8f8f8f' for _ in edges]
nx.draw_networkx_edges(G, pos, alpha=0.5, edge_color=edge_colors, min_source_margin=15, min_target_margin=15)

# Draw nodes with distinct sizes and colors
nx.draw_networkx_nodes(G, pos, node_size=node_sizes, node_color=node_colors, alpha=0.8)

# Draw labels with updated font size and weight
labels = {node: node for node in G.nodes()}
nx.draw_networkx_labels(G, pos, labels=labels, font_size=10)

plt.title("Dragon Lineage - Mother and Offspring", fontsize=14)
plt.axis('off')
plt.savefig('lineage_network_graph_updated.png')
plt.show()

The network graph above shows mother to offspring relationships, with each node (the circles in the chart) showing the scale color of each dragon.

EDA Summary

We've successfully established mother to offspring relatioships using source material, calculated potential mothers, and lifespan timelines. These are the following results:

mother_offspring_networkgraph

Network graph showing mother to offspring relationships, with each mother indicated by a red circle. This graph uses portraits of each dragon instead of only the scale colors.

Meraxes' pale scales show up repeatedly across generations, while Vhagar's bronze and green tones stay consistent. Balerion's black scales aren't as common, but his red eyes are noticeable, especially in younger dragons, suggesting red eyes might be recessive. The red from him appears in scales, accents, or eyes in nearly every generation. Vhagar seems to influence green and copper eye colors. Her green eyes mixed with Balerion's red eyes likely caused the copper eyes seen in Vermithor and Sheepstealer, which then passed to Vermithor's descendants, Meleys and Tessarion.

Both Silverwing and Dreamfyre paired with Vermithor, producing new color variations. Dreamfyre hatched Caraxes and Meleys, both bright red dragons, while Silverwing hatched Syrax and Sunfyre, the only yellow-toned dragons. Syrax is yellow, and Sunfyre is gold. Caraxes and Syrax share yellow traits, and Meleys and Sunfyre share metallic coloring, suggesting Vermithor's metallic bronze from Vhagar is a strong trait. Gold eyes from Meraxes appear sporadically. All three of Silverwing's offspring have varied scale and accent colors but inherit green eyes.

With the above information, we can move on to determining dominant and recessive traits, potential paternity, and predicting the physical appearances of dragons without canon descriptions.


The Genetics of Dragons

Before we can begin with assigning paternity, we have to establish trait dominance. In this section of the project, we'll be focusing on determining recessive and dominant hereditary traits in terms of scale colors, wings membrance & accent colors, & eye colors.

Wing & Accent Colors are the colors of wing membranes and/or crests and horns.

Scale Colors, Accent Colors, & Eye Colors

# Scale Colors

scale_data = pd.DataFrame({
    'Name': ['Arrax', 'Caraxes', 'Cannibal', 'Dreamfyre', 'Meleys', 'Moondancer', 'Morghul', 'Morning', 'Seasmoke', 'Sheepstealer', 'Shrykos', 'Silverwing', 
             'Stormcloud', 'Sunfyre', 'Syrax', 'Tessarion', 'Tyraxes', 'Vermax', 'Vermithor', 'Vhagar', 'Balerion', 'Meraxes', 'Quicksilver', 'Grey Ghost'],
    'HexCode_Scales': ['#F4EFE6', '#632322', '#181818', '#7AA2D0', '#8E1000', '#75968F', '#616269', '#B98293', '#DADADA', '#46322A', '#245550', '#bcc6cc', '#354669', 
              '#F1BD59', '#E4BD57', '#2F37A8', '#181818', '#929151', '#814C32', '#9A835A', '#181818', '#D3D4DB', '#F3ECEA', '#F2F2F2'],
})


wingmembrane_data = pd.DataFrame({
    'Name': ['Arrax', 'Caraxes', 'Cannibal', 'Dreamfyre', 'Meleys', 'Moondancer', 'Morghul', 'Morning', 'Seasmoke', 'Sheepstealer', 'Shrykos', 'Silverwing', 
             'Stormcloud', 'Sunfyre', 'Syrax', 'Tessarion', 'Tyraxes', 'Vermax', 'Vermithor', 'Vhagar', 'Balerion', 'Meraxes', 'Quicksilver', 'Grey Ghost'],
    'HexCode_Accents': ['#E5B347', '#632322', '#181818', '#bcc6cc', '#B87333', '#F4EFE6', '#616269', '#181818', '#CC9E9B', '#46322A', '#B87333', '#bcc6cc', '#2F2F2F', 
              '#CC9E9B', '#E4BD57', '#B87333', '#C50016', '#C0712C', '#C56731', '#4F635A', '#181818', '#CC9E9B', '#E5B347', '#F2F2F2']
})

eye_data = pd.DataFrame({
   'Name': ['Arrax', 'Caraxes', 'Cannibal', 'Dreamfyre', 'Meleys', 'Moondancer', 'Morghul', 'Morning', 'Seasmoke', 'Sheepstealer', 'Shrykos', 'Silverwing', 
             'Stormcloud', 'Sunfyre', 'Syrax', 'Tessarion', 'Tyraxes', 'Vermax', 'Vermithor', 'Vhagar', 'Balerion', 'Meraxes', 'Quicksilver', 'Grey Ghost'],
    'HexCode_Eyes': ['#E5B347', '#F5D21E', '#59A630', '#A6C3DC', '#B87333', '#98AF05', '#B83A36', '#B83A36', '#A6C3DC', '#C48C55', '#B87333', '#A6C3DC', '#A6C3DC', 
              '#5E7B36', '#5E7B36', '#B87333', '#B83A36', '#F5D21E', '#C48C55', '#5E7B36', '#B83A36', '#E5B347', '#E5B347', '#98AF05'],
})

# Sort the DataFrame by the 'Name' column
scale_data = scale_data.sort_values(by='Name').reset_index(drop=True)
wingmembrane_data = wingmembrane_data.sort_values(by='Name').reset_index(drop=True)
eye_data = eye_data.sort_values(by='Name').reset_index(drop=True)

# Function to convert hex color code to RGB
def hex_to_rgb(hex_code):
    return tuple(int(hex_code[i:i+2], 16)/255.0 for i in (1, 3, 5))

# Function to plot colors with names
def plot_colors_from_hex(names, hex_codes, title):
    fig, ax = plt.subplots(figsize=(12, 2))
    ax.set_title(title)
    for i, (name, hex_code) in enumerate(zip(names, hex_codes)):
        color = hex_to_rgb(hex_code)
        ax.add_patch(plt.Rectangle((i, 0), 1, 1, color=color))
        ax.text(i + 0.5, -0.1, name, ha='center', va='center', fontsize=10, color='black', rotation=90)
    ax.set_xlim(0, len(hex_codes))
    ax.set_ylim(-0.2, 1)
    ax.axis('off')
    plt.show()
    
# Plot the hex colors for scales with names
scales = plot_colors_from_hex(scale_data['Name'], scale_data['HexCode_Scales'], 'Scale Colors')
# Plot the hex colors for scales with names
accents = plot_colors_from_hex(wingmembrane_data['Name'], wingmembrane_data['HexCode_Accents'], 'Wing Membrane & Accent Colors')
# Plot the hex colors for scales with names
eyes = plot_colors_from_hex(eye_data['Name'], eye_data['HexCode_Eyes'], 'Eye Colors')

Determining Trait Dominance

To determine dominance, we will use the standardized color groups we made previously, then calculate which groups appear most frequently. Before we can do that, we need to create a new dataframe with the necessary lineage information like Father, Mother, and their associated color groups.

First we'll make the new dataframe by:

  1. Group colors in EyeColor by visual similarity.
  2. Assign bonded pairs as Father and Mother.
  3. Merge the DataFrames to link offspring with their parent color groups.
# Step 1: Group Colors by Visual Similarity
eye_assignments = {
    'Gold': 'Gold', 
    'Yellow': 'Yellow', 
    'Green': 'Green', 
    'Pale Blue': 'Blue', 
    'Bright Blue': 'Blue',
    'Bright Green': 'Green',
    'Copper': 'Copper', 
    'Pale Green': 'Green',
    'Red': 'Red',
    'Dark Green': 'Green'
}
df['EyeColorGroup'] = df['EyeColor'].map(eye_assignments)

# Step 2: Assign 'Father' and 'Mother' from Known Bonded Pairs
df['Mother'] = df['HatchedFrom']
bonded_pairs = [
    ('Balerion', 'Meraxes'), 
    ('Balerion', 'Vhagar'), 
    ('Vermithor', 'Dreamfyre'), 
    ('Vermithor', 'Silverwing'),
    ('Caraxes', 'Syrax'), 
]
bonded_pairs_df = pd.DataFrame(bonded_pairs, columns=['Father', 'Mother'])

# Step 3: Merge DataFrames to Link Offspring with Parent Color Groups
bonded_pairs_df = bonded_pairs_df.merge(df[['Name', 'ScaleColorGroup', 'AccentColorGroup', 'EyeColorGroup']], 
                                        left_on='Father', right_on='Name', how='left').drop(columns=['Name'])
bonded_pairs_df = bonded_pairs_df.merge(df[['Name', 'ScaleColorGroup', 'AccentColorGroup', 'EyeColorGroup']], 
                                        left_on='Mother', right_on='Name', how='left', suffixes=('_Father', '_Mother')).drop(columns=['Name'])
color_df = df.merge(bonded_pairs_df, left_on='Mother', right_on='Mother', how='left')
# Initial dataframes
dragon_data = pd.read_csv('final_cleaned_dragon_data.csv')
color_data = dragon_data[['Name', 'HatchedFrom', 'ScaleColor', 'WingMembraneColor', 'EyeColor']]

scale_data = pd.DataFrame({
    'Name': ['Arrax', 'Caraxes', 'Cannibal', 'Dreamfyre', 'Meleys', 'Moondancer', 'Morghul', 'Morning', 'Seasmoke', 'Sheepstealer', 'Shrykos', 'Silverwing', 
             'Stormcloud', 'Sunfyre', 'Syrax', 'Tessarion', 'Tyraxes', 'Vermax', 'Vermithor', 'Vhagar', 'Balerion', 'Meraxes', 'Quicksilver', 'Grey Ghost'],
    'HexCode_Scales': ['#F4EFE6', '#632322', '#181818', '#7AA2D0', '#8E1000', '#75968F', '#616269', '#B98293', '#DADADA', '#46322A', '#245550', '#bcc6cc', '#354669', 
              '#F1BD59', '#E4BD57', '#2F37A8', '#181818', '#929151', '#814C32', '#9A835A', '#181818', '#D3D4DB', '#F3ECEA', '#F2F2F2'],
})


accent_data = pd.DataFrame({
    'Name': ['Arrax', 'Caraxes', 'Cannibal', 'Dreamfyre', 'Meleys', 'Moondancer', 'Morghul', 'Morning', 'Seasmoke', 'Sheepstealer', 'Shrykos', 'Silverwing', 
             'Stormcloud', 'Sunfyre', 'Syrax', 'Tessarion', 'Tyraxes', 'Vermax', 'Vermithor', 'Vhagar', 'Balerion', 'Meraxes', 'Quicksilver', 'Grey Ghost'],
    'HexCode_Accents': ['#E5B347', '#632322', '#181818', '#bcc6cc', '#B87333', '#F4EFE6', '#616269', '#181818', '#CC9E9B', '#46322A', '#B87333', '#bcc6cc', '#2F2F2F', 
              '#CC9E9B', '#E4BD57', '#B87333', '#C50016', '#C0712C', '#C56731', '#4F635A', '#181818', '#CC9E9B', '#E5B347', '#F2F2F2']
})

eye_data = pd.DataFrame({
   'Name': ['Arrax', 'Caraxes', 'Cannibal', 'Dreamfyre', 'Meleys', 'Moondancer', 'Morghul', 'Morning', 'Seasmoke', 'Sheepstealer', 'Shrykos', 'Silverwing', 
            'Stormcloud', 'Sunfyre', 'Syrax', 'Tessarion', 'Tyraxes', 'Vermax', 'Vermithor', 'Vhagar', 'Balerion', 'Meraxes', 'Quicksilver', 'Grey Ghost'],
    'HexCode_Eyes': ['#E5B347', '#F5D21E', '#59A630', '#A6C3DC', '#B87333', '#98AF05', '#B83A36', '#B83A36', '#A6C3DC', '#C48C55', '#B87333', '#A6C3DC', '#A6C3DC', 
              '#5E7B36', '#5E7B36', '#B87333', '#B83A36', '#F5D21E', '#C48C55', '#5E7B36', '#B83A36', '#E5B347', '#E5B347', '#98AF05'],
})

# Converting dictionaries to DataFrames
scale_df = pd.DataFrame(scale_data)
accent_df = pd.DataFrame(accent_data)
eye_df = pd.DataFrame(eye_data)

# Merging dataframes
merged_df = scale_df.merge(accent_df, on='Name', how='left').merge(eye_df, on='Name', how='left')
color_df = merged_df.merge(color_data, on='Name', how='left')

# Alphabetize by 'Name'
color_df = color_df.sort_values(by='Name').reset_index(drop=True)

df = pd.DataFrame(color_df)
    
# Define specific groups based on visual similarity
scale_assignments = {
    'Silver': 'Silver',
    'Charcoal': 'Black',
    'Gold': 'Gold',
    'Bronze': 'Bronze',
    'Pearl-White': 'White',
    'Pale Silver-Grey': 'Silver',
    'Pale Silver-White': 'White',
    'Pale Grey-White': 'White',
    'Pale Grey': 'Grey',
    'Red': 'Red',
    'Scarlet Red': 'Red',
    'Pale Pink': 'Pink',
    'Black': 'Black',
    'Dark Grey': 'Black',
    'Brown': 'Bronze',
    'Pale Green': 'Green',
    'Olive Green': 'Green',
    'Jade Green': 'Green',
    'Green-Blue': 'Green',
    'Yellow': 'Yellow',
    'Dark Cobalt Blue': 'Blue',
    'Pale Blue': 'Blue',
    'Dark Grey-Blue': 'Blue'
}

accent_assignments = {
    'Gold': 'Gold', 
    'Red': 'Red', 
    'Black': 'Black', 
    'Silver': 'Silver', 
    'Pale Pink': 'Pink', 
    'Pearl-White': 'White',
    'Dark Grey': 'Black', 
    'Grey': 'Grey', 
    'Brown': 'Bronze', 
    'Green': 'Green', 
    'Yellow': 'Yellow', 
    'Pale Orange': 'Yellow', 
    'Tan': 'Bronze', 
    'Pale Grey': 'Grey',
    'Green-Blue': 'Green',
    'Pink': 'Pink',
    'Bronze': 'Bronze',
    'Charcoal': 'Black',
    'Pale Grey-White': 'White',
    'Copper': 'Copper',
    'White': 'White'
}

eye_assignments = {
    'Gold': 'Gold', 
    'Yellow': 'Yellow', 
    'Green': 'Green', 
    'Pale Blue': 'Blue', 
    'Bright Blue': 'Blue',
    'Bright Green': 'Green',
    'Copper': 'Copper', 
    'Pale Green': 'Green',
    'Red': 'Red',
    'Dark Green': 'Green'
}

# Apply the group assignments
df['ScaleColorGroup'] = df['ScaleColor'].map(scale_assignments)
df['AccentColorGroup'] = df['WingMembraneColor'].map(accent_assignments)
df['EyeColorGroup'] = df['EyeColor'].map(eye_assignments)

# Ensure 'Mother' is mapped using 'HatchedFrom'
df['Mother'] = df['HatchedFrom']

# List of known bonded pairs 
bonded_pairs = [
    ('Balerion', 'Meraxes'), 
    ('Balerion', 'Vhagar'), 
    ('Vermithor', 'Dreamfyre'), 
    ('Vermithor', 'Silverwing'),
    ('Caraxes', 'Syrax'), 
]

# Create a DataFrame from bonded pairs
bonded_pairs_df = pd.DataFrame(bonded_pairs, columns=['Father', 'Mother'])

# Merge color group information for fathers and mothers
bonded_pairs_df = bonded_pairs_df.merge(df[['Name', 'ScaleColorGroup', 'AccentColorGroup', 'EyeColorGroup']], left_on='Father', right_on='Name', how='left').drop(columns=['Name'])
bonded_pairs_df = bonded_pairs_df.merge(df[['Name', 'ScaleColorGroup', 'AccentColorGroup', 'EyeColorGroup']], left_on='Mother', right_on='Name', how='left', suffixes=('_Father', '_Mother')).drop(columns=['Name'])

# Merge color data with bonded pairs to associate offspring with both parents
color_df = df.merge(bonded_pairs_df, left_on='Mother', right_on='Mother', how='left')

# Ensure no nulls by filling with 'Unknown'
color_df['ScaleColorGroup_Father'] = color_df['Father'].map(df.set_index('Name')['ScaleColorGroup']).fillna('Unknown')
color_df['ScaleColorGroup_Mother'] = color_df['Mother'].map(df.set_index('Name')['ScaleColorGroup']).fillna('Unknown')
color_df['AccentColorGroup_Father'] = color_df['Father'].map(df.set_index('Name')['AccentColorGroup']).fillna('Unknown')
color_df['AccentColorGroup_Mother'] = color_df['Mother'].map(df.set_index('Name')['AccentColorGroup']).fillna('Unknown')
color_df['EyeColorGroup_Father'] = color_df['Father'].map(df.set_index('Name')['EyeColorGroup']).fillna('Unknown')
color_df['EyeColorGroup_Mother'] = color_df['Mother'].map(df.set_index('Name')['EyeColorGroup']).fillna('Unknown')

# Display a sample of the merged data to ensure correctness
color_df[['Name', 'ScaleColor', 'ScaleColorGroup', 'HexCode_Scales', 'WingMembraneColor', 'AccentColorGroup', 'HexCode_Accents', 'EyeColor', 'EyeColorGroup', 'HexCode_Eyes', 'Father', 'Mother', 'ScaleColorGroup_Father', 
          'ScaleColorGroup_Mother', 'AccentColorGroup_Father', 'AccentColorGroup_Mother', 'EyeColorGroup_Father', 'EyeColorGroup_Mother']].head()
Name ScaleColor ScaleColorGroup HexCode_Scales WingMembraneColor AccentColorGroup HexCode_Accents EyeColor EyeColorGroup HexCode_Eyes Father Mother ScaleColorGroup_Father ScaleColorGroup_Mother AccentColorGroup_Father AccentColorGroup_Mother EyeColorGroup_Father EyeColorGroup_Mother
0 Arrax Pearl-White White #F4EFE6 Gold Gold #E5B347 Gold Gold #E5B347 Caraxes Syrax Red Yellow Red Yellow Yellow Green
1 Balerion Black Black #181818 Black Black #181818 Red Red #B83A36 NaN Unknown Unknown Unknown Unknown Unknown Unknown Unknown
2 Cannibal Charcoal Black #181818 Black Black #181818 Bright Green Green #59A630 Balerion Vhagar Black Bronze Black Green Red Green
3 Caraxes Red Red #632322 Red Red #632322 Yellow Yellow #F5D21E Vermithor Dreamfyre Bronze Blue Bronze Silver Copper Blue
4 Dreamfyre Pale Blue Blue #7AA2D0 Silver Silver #bcc6cc Pale Blue Blue #A6C3DC NaN Quicksilver Unknown White Unknown Gold Unknown Gold
df = pd.DataFrame(color_df)

# Calculate the overall frequency of each trait in the entire dataset
scale_frequencies = df['ScaleColorGroup'].value_counts(normalize=True).to_dict()
accent_frequencies = df['AccentColorGroup'].value_counts(normalize=True).to_dict()
eye_frequencies = df['EyeColorGroup'].value_counts(normalize=True).to_dict()

# Calculate the frequency of each trait in parents
parent_scale_frequencies = df[['ScaleColorGroup_Father', 'ScaleColorGroup_Mother']].melt().value_counts(normalize=True).to_dict()
parent_accent_frequencies = df[['AccentColorGroup_Father', 'AccentColorGroup_Mother']].melt().value_counts(normalize=True).to_dict()
parent_eye_frequencies = df[['EyeColorGroup_Father', 'EyeColorGroup_Mother']].melt().value_counts(normalize=True).to_dict()

# Calculate the frequency of each trait in offspring
offspring_scale_frequencies = df['ScaleColorGroup'].value_counts(normalize=True).to_dict()
offspring_accent_frequencies = df['AccentColorGroup'].value_counts(normalize=True).to_dict()
offspring_eye_frequencies = df['EyeColorGroup'].value_counts(normalize=True).to_dict()

# Function to calculate generation span for each trait
def calculate_generation_span(df, trait_column, father_column, mother_column):
    generation_span = {}
    for name, group in df.groupby(trait_column):
        generations = set()
        for idx, row in group.iterrows():
            if row[father_column] == name or row[mother_column] == name:
                generations.add('parent')
            if row[trait_column] == name:
                generations.add('offspring')
        generation_span[name] = len(generations)
    return generation_span

# Calculate generation span for each trait
scale_generation_span = calculate_generation_span(df, 'ScaleColorGroup', 'ScaleColorGroup_Father', 'ScaleColorGroup_Mother')
accent_generation_span = calculate_generation_span(df, 'AccentColorGroup', 'AccentColorGroup_Father', 'AccentColorGroup_Mother')
eye_generation_span = calculate_generation_span(df, 'EyeColorGroup', 'EyeColorGroup_Father', 'EyeColorGroup_Mother')

# Function to determine dominance based on frequency and generation span
def determine_dominance(trait_frequencies, parent_trait_frequencies, offspring_trait_frequencies, generation_span):
    dominance_info = {}
    for trait, freq in trait_frequencies.items():
        parent_freq = parent_trait_frequencies.get(trait, 0)
        offspring_freq = offspring_trait_frequencies.get(trait, 0)
        gen_span = generation_span.get(trait, 0)
        
        if freq > 0.1 and gen_span > 1:  # Threshold for dominance
            dominance_info[trait] = 'Dominant'
        else:
            dominance_info[trait] = 'Recessive'
    return dominance_info

# Determine dominance for each trait type
scale_dominance_info = determine_dominance(scale_frequencies, parent_scale_frequencies, offspring_scale_frequencies, scale_generation_span)
accent_dominance_info = determine_dominance(accent_frequencies, parent_accent_frequencies, offspring_accent_frequencies, accent_generation_span)
eye_dominance_info = determine_dominance(eye_frequencies, parent_eye_frequencies, offspring_eye_frequencies, eye_generation_span)

def format_dominance_info(dominance_dict):
    dominant_colors = [color for color, dom in dominance_dict.items() if dom == 'Dominant']
    recessive_colors = [color for color, dom in dominance_dict.items() if dom == 'Recessive']
    return dominant_colors, recessive_colors

scale_dominant, scale_recessive = format_dominance_info(scale_dominance_info)
accent_dominant, accent_recessive = format_dominance_info(accent_dominance_info)
eye_dominant, eye_recessive = format_dominance_info(eye_dominance_info)

Now that we have the necessary information, we can move on to the final stage and determine dominance by:

# Step 1: Calculating the frequency of each trait in parents, offspring, and the entire dataset.
scale_frequencies = df['ScaleColorGroup'].value_counts(normalize=True).to_dict()
accent_frequencies = df['AccentColorGroup'].value_counts(normalize=True).to_dict()
eye_frequencies = df['EyeColorGroup'].value_counts(normalize=True).to_dict()

# Step 2: Create a function to determining how traits span across generations.
def calculate_generation_span(df, trait_column, father_column, mother_column):
    generation_span = {}
    for name, group in df.groupby(trait_column):
        generations = set()
        for idx, row in group.iterrows():
            if row[father_column] == name or row[mother_column] == name:
                generations.add('parent')
            if row[trait_column] == name:
                generations.add('offspring')
        generation_span[name] = len(generations)
    return generation_span

# Step 3: Create a function to determine dominance based on frequency and generation span
def determine_dominance(trait_frequencies, parent_trait_frequencies, offspring_trait_frequencies, generation_span):
    dominance_info = {}
    for trait, freq in trait_frequencies.items():
        parent_freq = parent_trait_frequencies.get(trait, 0)
        offspring_freq = offspring_trait_frequencies.get(trait, 0)
        gen_span = generation_span.get(trait, 0)

        if freq > 0.1 and gen_span > 1:  # Threshold for dominance
            dominance_info[trait] = 'Dominant'
        else:
            dominance_info[trait] = 'Recessive'
    return dominance_info

The Paternity of Dragons

We need to determine paternity for Dreamfyre, Silverwing, Seasmoke, Shrykos, and Morghul. We'll use the previously determined dominance information in combination with a machine learning model to assign paternity. The model we'll use is the Random Forests Model. It aggregates the results of multiple decision trees, giving us a more stable and reliable prediction compared to a single decision tree. This reduces the risk of overfitting and gives a more generalized prediction.

The steps we'll take to determine paternity will be: 1) Define traits of the offspring and their potential fathers.

# Define the traits of Seasmoke, Meleys, and potential fathers
seasmoke_traits = {'ScaleColorGroup': 'Silver', 'AccentColorGroup': 'Pink', 'EyeColorGroup': 'Blue'}

meleys_traits = {'ScaleColorGroup': 'Red', 'AccentColorGroup': 'Copper', 'EyeColorGroup': 'Copper'}

potential_fathers = pd.DataFrame({
    'Name': ['Sheepstealer', 'Cannibal'],
    'ScaleColorGroup': ['Bronze', 'Black'],
    'AccentColorGroup': ['Bronze', 'Black'],
    'EyeColorGroup': ['Copper', 'Green']
})

2) Compare the dominance information of the lineages and parents against the offspring.

# Grandparent traits data
grandparent_traits = {
    'Meleys': {'Father': {'ScaleColorGroup': 'Bronze', 'AccentColorGroup': 'Bronze', 'EyeColorGroup': 'Copper'},
                 'Mother': {'ScaleColorGroup': 'Blue', 'AccentColorGroup': 'Silver', 'EyeColorGroup': 'Blue'}},
    'Sheepstealer': {'Father': {'ScaleColorGroup': 'Black', 'AccentColorGroup': 'Black', 'EyeColorGroup': 'Red'},
                'Mother': {'ScaleColorGroup': 'Bronze', 'AccentColorGroup': 'Green', 'EyeColorGroup': 'Green'}},
    'Cannibal': {'Father': {'ScaleColorGroup': 'Black', 'AccentColorGroup': 'Black', 'EyeColorGroup': 'Red'},
                'Mother': {'ScaleColorGroup': 'Bronze', 'AccentColorGroup': 'Green', 'EyeColorGroup': 'Green'}}
}

# Dominance information
scale_dominance = scale_dominance_info
accent_dominance = accent_dominance_info
eye_dominance = eye_dominance_info

# Combine unique values from potential fathers and Seasmoke
combined_values = {
    'ScaleColorGroup': list(set(potential_fathers['ScaleColorGroup'].unique()).union(['Silver'])),
    'AccentColorGroup': list(set(potential_fathers['AccentColorGroup'].unique()).union(['Pink'])),
    'EyeColorGroup': list(set(potential_fathers['EyeColorGroup'].unique()).union(['Blue']))
}

3) Convert traits to numerical values to train the Random Forest model for paternity prediction.

# Encode categorical traits into numerical values
label_encoders = {col: LabelEncoder().fit(combined_values[col]) for col in combined_values}

# Transform the traits of potential fathers
encoded_fathers = potential_fathers.copy()
for col, le in label_encoders.items():
    encoded_fathers[col] = le.transform(potential_fathers[col])

# Prepare feature matrix (X) and target vector (y)
X = encoded_fathers[['ScaleColorGroup', 'AccentColorGroup', 'EyeColorGroup']]
y = encoded_fathers['Name']

4) Create functions for the model to run 100 iterations and determine the most predicted father.

# Function to fit the model and predict using Random Forest
def fit_and_predict_rf(traits):
    clf = RandomForestClassifier(random_state=42)
    clf.fit(X, y)
    encoded_traits = {col: le.transform([traits[col]])[0] for col, le in label_encoders.items()}
    traits_features = pd.DataFrame([encoded_traits])
    return clf.predict(traits_features)[0]

# Fit the Random Forest model multiple times and collect predictions
def collect_predictions(traits, iterations=100):
    predictions = [fit_and_predict_rf(traits) for _ in range(iterations)]
    return pd.Series(predictions).mode()[0]

# Determine the most predicted father for Seasmoke
most_predicted_father = collect_predictions(seasmoke_traits)

The Paternity of Dreamfyre and Silverwing

dreamfyre_silverwing

Dreamfyre (left) & Silverwing (right)

For Dreamfyre and Silverwing, the only logical options would be Balerion and Cannibal. I think Cannibal is the father of Dreamfyre and Silverwing based on a couple of different theories:

  • Size: Quicksilver only lived to be about 30-35 years old, dying in 10 AC. Balerion would have been 145 years old (or older), making him uncomfortably larger in size than Quicksilver. Without going into too much detail, I don't think coiling could've happened without Quicksilver being severely wounded or crushed by Balerion.
  • Cannibal was the oldest of the three wild dragons. Sheepstealer was said to have hatched between 40-51 AC, so Cannibal had to have hatched long before then, ~around 10-25 AC. This would've made him a decent age around the time of Dreamfyre and Silverwing's eggs hatching in 32 and 34 AC.
  • It's believed Cannibal hatched on Dragonstone, which is where Quicksilver resided with her rider and the Prince of Dragonstone, Prince Aenys Targaryen, until 37 AC. That timeline places Cannibal and Quicksilver both on Dragonstone.

balerion_cannibal

Possible fathers of Dreamfyre & Silverwing: Balerion (left) & Cannibal (right)

# Define the traits of Dreamfyre, Silverwing, and potential fathers
dreamfyre_traits = {'ScaleColorGroup': 'Blue', 'AccentColorGroup': 'Silver', 'EyeColorGroup': 'Blue'}
silverwing_traits = {'ScaleColorGroup': 'Silver', 'AccentColorGroup': 'Silver', 'EyeColorGroup': 'Blue'}
quicksilver_traits = {'ScaleColorGroup': 'White', 'AccentColorGroup': 'Gold', 'EyeColorGroup': 'Gold'}

potential_fathers = pd.DataFrame({
    'Name': ['Balerion', 'Cannibal'],
    'ScaleColorGroup': ['Black', 'Black'],
    'AccentColorGroup': ['Black', 'Black'],
    'EyeColorGroup': ['Red', 'Green']
})

# Show the results for Dreamfyre and Silverwing
dreamfyre_silverwing_result = color_df[color_df['Name'].isin(['Dreamfyre', 'Silverwing'])][['Name', 'Mother', 'Father']]
df = pd.DataFrame(color_df)

# Define the traits of Dreamfyre, Silverwing, and potential fathers
dreamfyre_traits = {'ScaleColorGroup': 'Blue', 'AccentColorGroup': 'Silver', 'EyeColorGroup': 'Blue'}
silverwing_traits = {'ScaleColorGroup': 'Silver', 'AccentColorGroup': 'Silver', 'EyeColorGroup': 'Blue'}

quicksilver_traits = {'ScaleColorGroup': 'White', 'AccentColorGroup': 'Gold', 'EyeColorGroup': 'Gold'}

potential_fathers = pd.DataFrame({
    'Name': ['Balerion', 'Cannibal'],
    'ScaleColorGroup': ['Black', 'Black'],
    'AccentColorGroup': ['Black', 'Black'],
    'EyeColorGroup': ['Red', 'Green']
})

# Dominance information
scale_dominance = scale_dominance_info
accent_dominance = accent_dominance_info
eye_dominance = eye_dominance_info

# Combine unique values from potential fathers and offspring traits
combined_values = {
    'ScaleColorGroup': list(set(potential_fathers['ScaleColorGroup'].unique()).union(['Blue', 'Silver'])),
    'AccentColorGroup': list(set(potential_fathers['AccentColorGroup'].unique()).union(['Silver', 'Silver'])),
    'EyeColorGroup': list(set(potential_fathers['EyeColorGroup'].unique()).union(['Blue', 'Blue']))
}

# Encode categorical traits into numerical values
label_encoders = {col: LabelEncoder().fit(combined_values[col]) for col in combined_values}

# Transform the traits of potential fathers
encoded_fathers = potential_fathers.copy()
for col, le in label_encoders.items():
    encoded_fathers[col] = le.transform(potential_fathers[col])

# Prepare feature matrix (X) and target vector (y)
X = encoded_fathers[['ScaleColorGroup', 'AccentColorGroup', 'EyeColorGroup']]
y = encoded_fathers['Name']

# Function to fit the model and predict using Random Forest
def fit_and_predict_rf(traits):
    clf = RandomForestClassifier(random_state=42)
    clf.fit(X, y)
    encoded_traits = {col: le.transform([traits[col]])[0] for col, le in label_encoders.items()}
    traits_features = pd.DataFrame([encoded_traits])
    return clf.predict(traits_features)[0]

# Fit the Random Forest model multiple times and collect predictions
def collect_predictions(traits, iterations=100):
    predictions = [fit_and_predict_rf(traits) for _ in range(iterations)]
    return pd.Series(predictions).mode()[0]

# Determine the most predicted father for each offspring
most_predicted_fathers = {
    'Dreamfyre': collect_predictions(dreamfyre_traits),
    'Silverwing': collect_predictions(silverwing_traits)
}

# Update color_df with the predicted fathers and their traits
for offspring, father in most_predicted_fathers.items():
    color_df.loc[color_df['Name'] == offspring, 'Father'] = father
    father_traits = potential_fathers[potential_fathers['Name'] == father].iloc[0]
    color_df.loc[color_df['Name'] == offspring, 'ScaleColorGroup_Father'] = father_traits['ScaleColorGroup']
    color_df.loc[color_df['Name'] == offspring, 'AccentColorGroup_Father'] = father_traits['AccentColorGroup']
    color_df.loc[color_df['Name'] == offspring, 'EyeColorGroup_Father'] = father_traits['EyeColorGroup']
    mother_traits = quicksilver_traits
    color_df.loc[color_df['Name'] == offspring, 'ScaleColorGroup_Mother'] = mother_traits['ScaleColorGroup']
    color_df.loc[color_df['Name'] == offspring, 'AccentColorGroup_Mother'] = mother_traits['AccentColorGroup']
    color_df.loc[color_df['Name'] == offspring, 'EyeColorGroup_Mother'] = mother_traits['EyeColorGroup']

# Show the results for Dreamfyre and Silverwing
dreamfyre_silverwing_result = color_df[color_df['Name'].isin(['Dreamfyre', 'Silverwing'])][['Name', 'Mother', 'Father']]
Name Mother Father
Dreamfyre Quicksilver Cannibal
Silverwing Quicksilver Cannibal

The Paternity of Seasmoke

seasmoke

Seasmoke

Based on timeline and ages, the only male dragons of the appropriate age and in the same general location as Meleys would've been Balerion, Caraxes, Cannibal, Vermithor, and Sheepstealer. Seasmoke was hatched around 97 AC. With that in mind, I think the only possible fathers could be either Cannibal or Sheepstealer. Here's why:

  • Caraxes was in the Stepstones with his rider Daemon Targaryen from 96 AC - 99 AC. and Meleys would've been in Driftmark where her rider, Princess Rhaenys Targaryen, resided with her husband, Lord Corlys Velaryon. So Caraxes wouldn't have been around Meleys at all.
  • Vermithor resided in the Dragonpit while Meleys was in Driftmark. Like Caraxes, lack of proximity excludes Vermithor from being a potential father to Seasmoke.
  • Balerion died in 94 AC, before Seasmoke's egg was taken and given as a cradle egg to Princess Rhaenys' son, Ser Laenor Velaryon in 97 AC.
  • Seasmoke is described as "nimble in the air", which is more in-line with Sheepstealer's description as a "slender mud brown dragon" This direct quote also places Sheepstealer in Driftmark with Meleys:

    "In Fire & Blood, Sheepstealer was a slender mud brown dragon with a taste for mutton. He hatched sometime during the youth of Jaehaerys I Targaryen and resided within the Dragonmont, hunting sheep as far as Driftmark and the Wendwater."

    -Wiki of Westeros

We will use Sheepstealer and Cannibal as viable options for paternity for Seasmoke

sheepstealer_cannibal

Possible fathers of Seasmoke: Sheepstealer (left) & Cannibal (right)

# Define the traits of Seasmoke, Meleys, and potential fathers
seasmoke_traits = {'ScaleColorGroup': 'Silver', 'AccentColorGroup': 'Pink', 'EyeColorGroup': 'Blue'}

meleys_traits = {'ScaleColorGroup': 'Red', 'AccentColorGroup': 'Copper', 'EyeColorGroup': 'Copper'}

potential_fathers = pd.DataFrame({
    'Name': ['Sheepstealer', 'Cannibal'],
    'ScaleColorGroup': ['Bronze', 'Black'],
    'AccentColorGroup': ['Bronze', 'Black'],
    'EyeColorGroup': ['Copper', 'Green']
})                      

# Show the results for Seasmoke
seasmoke_result = color_df[color_df['Name'].isin(['Seasmoke'])][['Name', 'Mother', 'Father']]
# Define the traits of Seasmoke, Meleys, and potential fathers
seasmoke_traits = {'ScaleColorGroup': 'Silver', 'AccentColorGroup': 'Pink', 'EyeColorGroup': 'Blue'}

meleys_traits = {'ScaleColorGroup': 'Red', 'AccentColorGroup': 'Copper', 'EyeColorGroup': 'Copper'}

potential_fathers = pd.DataFrame({
    'Name': ['Sheepstealer', 'Cannibal'],
    'ScaleColorGroup': ['Bronze', 'Black'],
    'AccentColorGroup': ['Bronze', 'Black'],
    'EyeColorGroup': ['Copper', 'Green']
})

# Grandparent traits data
grandparent_traits = {
    'Meleys': {'Father': {'ScaleColorGroup': 'Bronze', 'AccentColorGroup': 'Bronze', 'EyeColorGroup': 'Copper'},
                 'Mother': {'ScaleColorGroup': 'Blue', 'AccentColorGroup': 'Silver', 'EyeColorGroup': 'Blue'}},
    'Sheepstealer': {'Father': {'ScaleColorGroup': 'Black', 'AccentColorGroup': 'Black', 'EyeColorGroup': 'Red'},
                'Mother': {'ScaleColorGroup': 'Bronze', 'AccentColorGroup': 'Green', 'EyeColorGroup': 'Green'}},
    'Cannibal': {'Father': {'ScaleColorGroup': 'Black', 'AccentColorGroup': 'Black', 'EyeColorGroup': 'Red'},
                'Mother': {'ScaleColorGroup': 'Bronze', 'AccentColorGroup': 'Green', 'EyeColorGroup': 'Green'}}
}

# Dominance information
scale_dominance = scale_dominance_info
accent_dominance = accent_dominance_info
eye_dominance = eye_dominance_info


# Combine unique values from potential fathers and Seasmoke
combined_values = {
    'ScaleColorGroup': list(set(potential_fathers['ScaleColorGroup'].unique()).union(['Silver'])),
    'AccentColorGroup': list(set(potential_fathers['AccentColorGroup'].unique()).union(['Pink'])),
    'EyeColorGroup': list(set(potential_fathers['EyeColorGroup'].unique()).union(['Blue']))
}

# Encode categorical traits into numerical values
label_encoders = {col: LabelEncoder().fit(combined_values[col]) for col in combined_values}

# Transform the traits of potential fathers
encoded_fathers = potential_fathers.copy()
for col, le in label_encoders.items():
    encoded_fathers[col] = le.transform(potential_fathers[col])

# Prepare feature matrix (X) and target vector (y)
X = encoded_fathers[['ScaleColorGroup', 'AccentColorGroup', 'EyeColorGroup']]
y = encoded_fathers['Name']

# Function to fit the model and predict using Random Forest
def fit_and_predict_rf(traits):
    clf = RandomForestClassifier(random_state=42)
    clf.fit(X, y)
    encoded_traits = {col: le.transform([traits[col]])[0] for col, le in label_encoders.items()}
    traits_features = pd.DataFrame([encoded_traits])
    return clf.predict(traits_features)[0]

# Fit the Random Forest model multiple times and collect predictions
def collect_predictions(traits, iterations=100):
    predictions = [fit_and_predict_rf(traits) for _ in range(iterations)]
    return pd.Series(predictions).mode()[0]

# Determine the most predicted father for Seasmoke
most_predicted_father = collect_predictions(seasmoke_traits)

# Update color_df with the predicted father and traits for Seasmoke
color_df.loc[color_df['Name'] == 'Seasmoke', 'Father'] = most_predicted_father
father_traits = potential_fathers[potential_fathers['Name'] == most_predicted_father].iloc[0]
color_df.loc[color_df['Name'] == 'Seasmoke', 'ScaleColorGroup_Father'] = father_traits['ScaleColorGroup']
color_df.loc[color_df['Name'] == 'Seasmoke', 'AccentColorGroup_Father'] = father_traits['AccentColorGroup']
color_df.loc[color_df['Name'] == 'Seasmoke', 'EyeColorGroup_Father'] = father_traits['EyeColorGroup']
mother_traits = meleys_traits
color_df.loc[color_df['Name'] == 'Seasmoke', 'ScaleColorGroup_Mother'] = mother_traits['ScaleColorGroup']
color_df.loc[color_df['Name'] == 'Seasmoke', 'AccentColorGroup_Mother'] = mother_traits['AccentColorGroup']
color_df.loc[color_df['Name'] == 'Seasmoke', 'EyeColorGroup_Mother'] = mother_traits['EyeColorGroup']

# Show the results for Seasmoke
seasmoke_result = color_df[color_df['Name'].isin(['Seasmoke'])][['Name', 'Mother', 'Father']]
Name Mother Father
Seasmoke Meleys Sheepstealer

The Paternity of Shrykos and Morghul

shrykos_morghul

Shrykos (left) & Morghul (right)

Tessarion was between Oldtown, where her rider Daeron Targaryen was sent to be a squire for his uncle Lord Hobert Hightower, and King's Landing where she stayed in the Dragonpit. The best fit in terms of proximity to Tessarion, age, and timeline are either Vermax, Grey Ghost, Sheepstealer, Cannibal, or Sunfyre.

My bets are on Sunfyre being the father of Shrykos and Morghul. Here's why:

  • Sunfyre was the only male dragon of a reasonably sexually mature age residing in the dragonpit on Kings Landing between 110 AC and 123 AC.
  • With Vermax being split between Winterfell and Dragonstone, it's highly unlikely Vermax and Tessarion would have been anywhere near each other in terms of location and timelines. It's also possible that Vermax might be a She-Dragon. It's rumored that while in Winterfell, Vermax laid a clutch of eggs that never hatched because of the cold, Northern snow.

    "...Mushroom claims in his Testimony that the dragon Vermax left a clutch of eggs somewhere in the depths of Winterfell’s crypts, where the waters of the hot springs run close to the walls, while his rider treated with Cregan Stark at the start of the Dance of the Dragons."

    -The World of Ice & Fire, p. 332

  • Grey Ghost, Sheepstealer, and Cannibal were wild dragons while Tessarion resided in the Dragonpit. It is unlikely any of them are the potential father. Sheepstealer and Cannibal were known to stay between Dragonstone & Driftmark, nowhere near Tessarion. This makes Grey Ghost the only remaining option, even though I still think it is unlikely.

So, we will use Grey Ghost and Sunfyre as potential fathers to Shrykos and Morgul.

greyghost_sunfyre

Grey Ghost (left) & Sunfyre (right)

# Define the traits of Stormcloud, Morghul, Tessarion, and potential fathers
shrykos_traits = {'ScaleColorGroup': 'Green', 'AccentColorGroup': 'Copper', 'EyeColorGroup': 'Copper'}
morghul_traits = {'ScaleColorGroup': 'Black', 'AccentColorGroup': 'Black', 'EyeColorGroup': 'Red'}

tessarion_traits = {'ScaleColorGroup': 'Blue', 'AccentColorGroup': 'Copper', 'EyeColorGroup': 'Copper'}

potential_fathers = pd.DataFrame({
    'Name': ['Grey Ghost', 'Sunfyre'],
    'ScaleColorGroup': ['White', 'Gold'],
    'AccentColorGroup': ['White', 'Pink'],
    'EyeColorGroup': ['Green', 'Green']
})                   
# Show the results for Stormcloud and Morghul
shrykos_morghul_result = color_df[color_df['Name'].isin(['Shrykos', 'Morghul'])][['Name', 'Mother', 'Father']]
# Define the traits of Shrykos, Morghul, Tessarion, and potential fathers
shrykos_traits = {'ScaleColorGroup': 'Green', 'AccentColorGroup': 'Copper', 'EyeColorGroup': 'Copper'}
morghul_traits = {'ScaleColorGroup': 'Black', 'AccentColorGroup': 'Black', 'EyeColorGroup': 'Red'}

tessarion_traits = {'ScaleColorGroup': 'Blue', 'AccentColorGroup': 'Copper', 'EyeColorGroup': 'Copper'}

potential_fathers = pd.DataFrame({
    'Name': ['Grey Ghost', 'Sunfyre'],
    'ScaleColorGroup': ['White', 'Gold'],
    'AccentColorGroup': ['White', 'Pink'],
    'EyeColorGroup': ['Green', 'Green']
})

# Grandparent traits data
grandparent_traits = {
    'Tessarion': {'Father': {'ScaleColorGroup': 'Bronze', 'AccentColorGroup': 'Bronze', 'EyeColorGroup': 'Copper'},
                 'Mother': {'ScaleColorGroup': 'Blue', 'AccentColorGroup': 'Silver', 'EyeColorGroup': 'Blue'}},
    'Grey Ghost': {'Father': {'ScaleColorGroup': 'Bronze', 'AccentColorGroup': 'Bronze', 'EyeColorGroup': 'Copper'},
                'Mother': {'ScaleColorGroup': 'Silver', 'AccentColorGroup': 'Silver', 'EyeColorGroup': 'Blue'}},
    'Sunfyre': {'Father': {'ScaleColorGroup': 'Bronze', 'AccentColorGroup': 'Bronze', 'EyeColorGroup': 'Copper'},
                'Mother': {'ScaleColorGroup': 'Silver', 'AccentColorGroup': 'Silver', 'EyeColorGroup': 'Blue'}},
}

# Dominance information
scale_dominance = scale_dominance_info

accent_dominance = accent_dominance_info

eye_dominance = eye_dominance_info

# Combine values from potential fathers and offspring traits
combined_values = {
    'ScaleColorGroup': list(set(potential_fathers['ScaleColorGroup'].unique()).union(['Green', 'Black'])),
    'AccentColorGroup': list(set(potential_fathers['AccentColorGroup'].unique()).union(['Copper', 'Black'])),
    'EyeColorGroup': list(set(potential_fathers['EyeColorGroup'].unique()).union(['Copper', 'Red']))
}

# Encode categorical traits into numerical values
label_encoders = {col: LabelEncoder().fit(combined_values[col]) for col in combined_values}

# Transform the traits of potential fathers
encoded_fathers = potential_fathers.copy()
for col, le in label_encoders.items():
    encoded_fathers[col] = le.transform(potential_fathers[col])

# Prepare feature matrix (X) and target vector (y)
X = encoded_fathers[['ScaleColorGroup', 'AccentColorGroup', 'EyeColorGroup']]
y = encoded_fathers['Name']

# Function to fit the model and predict using Random Forest
def fit_and_predict_rf(traits):
    clf = RandomForestClassifier(random_state=42)
    clf.fit(X, y)
    encoded_traits = {col: le.transform([traits[col]])[0] for col, le in label_encoders.items()}
    traits_features = pd.DataFrame([encoded_traits])
    return clf.predict(traits_features)[0]

# Fit the Random Forest model multiple times and collect predictions
def collect_predictions(traits, iterations=100):
    predictions = [fit_and_predict_rf(traits) for _ in range(iterations)]
    return pd.Series(predictions).mode()[0]

# Determine the most predicted father for each offspring
most_predicted_fathers = {
    'Shrykos': collect_predictions(shrykos_traits),
    'Morghul': collect_predictions(morghul_traits)
}

# Update color_df with the predicted fathers and their traits
for offspring, father in most_predicted_fathers.items():
    color_df.loc[color_df['Name'] == offspring, 'Father'] = father
    father_traits = potential_fathers[potential_fathers['Name'] == father].iloc[0]
    color_df.loc[color_df['Name'] == offspring, 'ScaleColorGroup_Father'] = father_traits['ScaleColorGroup']
    color_df.loc[color_df['Name'] == offspring, 'AccentColorGroup_Father'] = father_traits['AccentColorGroup']
    color_df.loc[color_df['Name'] == offspring, 'EyeColorGroup_Father'] = father_traits['EyeColorGroup']
    mother_traits = tessarion_traits
    color_df.loc[color_df['Name'] == offspring, 'ScaleColorGroup_Mother'] = mother_traits['ScaleColorGroup']
    color_df.loc[color_df['Name'] == offspring, 'AccentColorGroup_Mother'] = mother_traits['AccentColorGroup']
    color_df.loc[color_df['Name'] == offspring, 'EyeColorGroup_Mother'] = mother_traits['EyeColorGroup']

# Show the results for Stormcloud and Morghul
shrykos_morghul_result = color_df[color_df['Name'].isin(['Shrykos', 'Morghul'])][['Name', 'Mother', 'Father']]
Name Mother Father
Morghul Tessarion Sunfyre
Shrykos Tessarion Sunfyre

Dragon Paternity Results

Name Mother Father
Arrax Syrax Caraxes
Cannibal Vhagar Balerion
Caraxes Dreamfyre Vermithor
Dreamfyre Quicksilver Cannibal
Grey Ghost Silverwing Vermithor
Meleys Dreamfyre Vermithor
Moondancer Syrax Caraxes
Morghul Tessarion Sunfyre
Morning Syrax Caraxes
Quicksilver Meraxes Balerion
Seasmoke Meleys Sheepstealer
Sheepstealer Vhagar Balerion
Shrykos Tessarion Sunfyre
Silverwing Quicksilver Cannibal
Stormcloud Syrax Caraxes
Sunfyre Silverwing Vermithor
Syrax Silverwing Vermithor
Tessarion Dreamfyre Vermithor
Tyraxes Syrax Caraxes
Vermax Syrax Caraxes
Vermithor Vhagar Balerion

The Predictions of Dragons

There are some dragons in the dataset whose appearances are only vaguely described, if mentioned at all, in Fire & Blood, The World of Ice & Fire, or The Rise of the Dragon. For example: Shrykos, Morghul, Stormcloud, Vermax, and Tyraxes. Now that we've established the likely parents of each dragon, I'm curious to see if we can use that information to also predict the most likely scale, accent, and eye colors for the dragons without specific descriptions in the books.

These are the exact steps we'll take:

1) Use hex_to_rgb and rgb_to_hex functions for color conversions.

# Step 1: Helper functions for color conversions
def hex_to_rgb(hex_code):
    return np.array([int(hex_code[i:i+2], 16) for i in (1, 3, 5)])  # Skip '#' and convert
def rgb_to_hex(rgb):
    clipped_rgb = np.clip(rgb, 0, 255)
    return '#{:02x}{:02x}{:02x}'.format(int(clipped_rgb[0]), int(clipped_rgb[1]), int(clipped_rgb[2]))

2) Prepare the feature vector from ancestor hex codes.

# Step 2: Prepare the feature vector from ancestor hex codes
def prepare_features(parent_hex1, parent_hex2, grandparents_hex, great_grandparents_hex):
    features = {}
    for feature in parent_hex1:
        parent_rgb1 = hex_to_rgb(parent_hex1[feature])
        parent_rgb2 = hex_to_rgb(parent_hex2[feature])
        grandparents_rgb = [hex_to_rgb(grandparents_hex[parent][feature]) for parent in ['Father', 'Mother1', 'Mother2']]
        great_grandparents_rgb = [hex_to_rgb(great_grandparents_hex[grandparent][feature]) for grandparent in ['Great_Father', 'Great_Mother', 'Great_Father2', 'Great_Mother2']]
        feature_vector = np.concatenate([parent_rgb1, parent_rgb2] + grandparents_rgb + great_grandparents_rgb)
        features[feature] = feature_vector
    return features

3) Blend RGB values from ancestors with some noise to simulate genetic variations.

  • The weights weights = np.array([0.2, 0.2, 0.1, 0.1, 0.1, 0.1, 0.05, 0.05, 0.05, 0.05]) correspond to each ancestor's influence on the color. They determine how much influence each dragon has on the blended RGB value when predicting the offspring's color characteristics.
    # Step 3: Blend RGB values from ancestors with some noise
    def blend_colors(*args):
      weights = np.array([0.2, 0.2, 0.1, 0.1, 0.1, 0.1, 0.05, 0.05, 0.05, 0.05])  # Adjusted weights for parents, grandparents, and great-grandparents
      blended_rgb = sum(weight * color for weight, color in zip(weights, args))
      blended_rgb += np.random.normal(0, 29, blended_rgb.shape)  # Add some noise
      return rgb_to_hex(np.clip(blended_rgb, 0, 255))
    
    4) Create functions that blend the results to predict the colors for each trait.
    # Step 4: Predict colors for each feature group for each offspring separately.
    def predict_offspring_colors(parent_hex1, parent_hex2, grandparents_hex, great_grandparents_hex):
      predicted_colors = {}
      for feature in parent_hex1:
          parent_color1 = parent_hex1[feature]
          parent_color2 = parent_hex2[feature]
          grandparent_colors = [grandparents_hex[parent][feature] for parent in ['Father', 'Mother1', 'Mother2']]
          great_grandparent_colors = [great_grandparents_hex[grandparent][feature] for grandparent in ['Great_Father', 'Great_Mother', 'Great_Father2', 'Great_Mother2']]
          predicted_colors[feature] = blend_colors(hex_to_rgb(parent_color1), hex_to_rgb(parent_color2), *[hex_to_rgb(color) for color in grandparent_colors + great_grandparent_colors])
      return predicted_colors
    

What Color are Shrykos and Morghul?

Shrykos and Morghul's appearance aren't stated in any canon source material. Aside from how they die, not much is known about these two dragons. What we do know is that:

  • Shrykos was bonded to Jaehaerys and Morghul to Jaehaera, They were the twin son and daughter of King Aegon II and Queen Helaena Targaryen.
  • "Morghul" means "death" in High Valyrian. So we can probably assume he has darker, more sinister tones.
  • Shrykos is a young She-Dragon whose dragon-egg was a green color.

The colors I've used in the dataset are based on the illustrations of them in The Rise of the Dragon, shown below.

Rise of the Dragon Illustrations

Illustration by Campbell White in "The Rise of the Dragon," p. 257: Morghul in "The Storming of the Dragonpit" (left).
Illustration by Marc Simonetti in "The Rise of the Dragon," p. 258: Shrykos in "The Destruction of the Dragonpit" (right).

  1. "The Rise of the Dragon", p. 257, Morghul: Knowing that Dreamfyre is that light blue colored dragon in the top lefthand corner of the image, we can assume that this is Morghul depicted as entirely greyish with bright red eyes.

  2. "The Rise of the Dragon", p. 258, Shrykos: It's written the "Tyraxes, older than the others, was more cunning, retreating to his lair and forcing his would-be killers to approach through the corpse-filled entrance..." (Rise of the Dragon, p. 256). With Morghul and Dreamfyre being accounted for, that leaves Shrykos as the only possible dragon in this image. We can see Shrykos is shown with an emerald green coloring.

The scale, accents, and eye colors I used for Shrykos and Morghul are:

shrykos_morghul

Shrykos (left) & Morghul (right)

Name ScaleColor WingMembraneColor EyeColor
Morghul Dark Grey Dark Grey Red
Shrykos Jade Green Copper Copper

Because of the illustration and the meaning or Morghul's name, I'm fairly confident in the colors I already have for him.

I'm confident in Shyrkos' coloring as well, but because the picture has a bit of an overlay, I'll be validating my color choices by calculating her coloring based off of the color traits within her lineage and seeing if they align well.

# Color data
data = [
    ['Arrax', '#F4EFE6', '#E5B347', '#E5B347', 'Syrax', 'Seasmoke'],
    ['Caraxes', '#C50016', '#C50016', '#F5D21E', 'Dreamfyre', 'Vermithor'],
    ['Cannibal', '#181818', '#181818', '#59A630', 'Vhagar', 'Balerion'],
    ['Dreamfyre', '#2F37A8', '#bcc6cc', '#A6C3DC', 'Quicksilver', 'Cannibal'],
    ['Meleys', '#B22A30', '#B87333', '#B87333', 'Dreamfyre', 'Vermithor'],
    ['Moondancer', '#8A9C74', '#F4EFE6', '#5E7B36', 'Syrax', 'Caraxes'],
    ['Morghul', '#2F2F2F', '#2F2F2F', '#B83A36', 'Tessarion', 'Sunfyre'],
    ['Morning', '#CC9E9B', '#181818', '#B83A36', 'Syrax', 'Caraxes'],
    ['Seasmoke', '#DADADA', '#CC9E9B', '#A6C3DC', 'Meleys', 'Sheepstealer'],
    ['Sheepstealer', '#4A3931', '#4A3931', '#C48C55', 'Vhagar', 'Balerion'],
    ['Shrykos', '#033537', '#B87333', '#B87333', 'Tessarion', 'Sunfyre'],
    ['Silverwing', '#bcc6cc', '#bcc6cc', '#A6C3DC', 'Quicksilver', 'Cannibal'],
    ['Stormcloud', '#354669', '#2F2F2F', '#A6C3DC', 'Syrax', 'Seasmoke'],
    ['Sunfyre', '#E5B347', '#CC9E9B', '#5E7B36', 'Silverwing', 'Vermithor'],
    ['Syrax', '#F5D21E', '#F5D21E', '#5E7B36', 'Silverwing', 'Vermithor'],
    ['Tessarion', '#2F37A8', '#B87333', '#B87333', 'Dreamfyre', 'Vermithor'],
    ['Tyraxes', '#181818', '#C50016', '#B83A36', 'Syrax', 'Caraxes'],
    ['Vermax', '#929151', '#F18763', '#F5D21E', 'Syrax', 'Caraxes'],
    ['Vermithor', '#A77F5B', '#C56731', '#C48C55', 'Vhagar', 'Balerion'],
    ['Vhagar', '#9A835A', '#4F635A', '#5E7B36', 'Unknown', 'NaN'],
    ['Balerion', '#181818', '#181818', '#B83A36', 'Unknown', 'NaN'],
    ['Meraxes', '#D3D4DB', '#CC9E9B', '#E5B347', 'Unknown', 'NaN'],
    ['Quicksilver', '#F3ECEA', '#E5B347', '#E5B347', 'Meraxes', 'Balerion'],
    ['Grey Ghost', '#F1F1F1', '#F1F1F1', '#59A630', 'Silverwing', 'Vermithor']
]

# Convert the data into a DataFrame
df = pd.DataFrame(data, columns=['Name', 'HexCode_Scales', 'HexCode_Accents', 'HexCode_Eyes', 'Mother', 'Father'])

# Sort the DataFrame by the 'Name' column
df_sorted = df.sort_values(by='Name').reset_index(drop=True)

# Function to convert hex to RGB
def hex_to_rgb(hex_code):
    hex_code = hex_code.lstrip('#')
    return [int(hex_code[i:i+2], 16) for i in (0, 2, 4)]

# Convert hex codes to RGB values after sorting
df_sorted['Scales_RGB'] = df_sorted['HexCode_Scales'].apply(hex_to_rgb)
df_sorted['Accents_RGB'] = df_sorted['HexCode_Accents'].apply(hex_to_rgb)
df_sorted['Eyes_RGB'] = df_sorted['HexCode_Eyes'].apply(hex_to_rgb)
# Helper functions for color conversions
def hex_to_rgb(hex_code):
    return np.array([int(hex_code[i:i+2], 16) for i in (1, 3, 5)])  # Skip '#' and convert

def rgb_to_hex(rgb):
    clipped_rgb = np.clip(rgb, 0, 255)
    return '#{:02x}{:02x}{:02x}'.format(int(clipped_rgb[0]), int(clipped_rgb[1]), int(clipped_rgb[2]))

# Prepare the feature vector from ancestor hex codes
def prepare_features(parent_hex1, parent_hex2, grandparents_hex, great_grandparents_hex):
    features = {}
    for feature in parent_hex1:
        parent_rgb1 = hex_to_rgb(parent_hex1[feature])
        parent_rgb2 = hex_to_rgb(parent_hex2[feature])
        grandparents_rgb = [hex_to_rgb(grandparents_hex[parent][feature]) for parent in ['Father', 'Mother1', 'Mother2']]
        great_grandparents_rgb = [hex_to_rgb(great_grandparents_hex[grandparent][feature]) for grandparent in ['Great_Father', 'Great_Mother', 'Great_Father2', 'Great_Mother2']]
        feature_vector = np.concatenate([parent_rgb1, parent_rgb2] + grandparents_rgb + great_grandparents_rgb)
        features[feature] = feature_vector
    return features

# Blend RGB values from ancestors with some noise
def blend_colors(*args):
    weights = np.array([0.2, 0.2, 0.1, 0.1, 0.1, 0.1, 0.05, 0.05, 0.05, 0.05]) # Adjusted weights for parents, grandparents, and great-grandparents
    blended_rgb = sum(weight * color for weight, color in zip(weights, args))
    blended_rgb += np.random.normal(0, 29, blended_rgb.shape)  # Add some noise
    return rgb_to_hex(np.clip(blended_rgb, 0, 255))

# Predict colors for each feature group for each offspring separately
def predict_offspring_colors(parent_hex1, parent_hex2, grandparents_hex, great_grandparents_hex):
    predicted_colors = {}
    for feature in parent_hex1:
        parent_color1 = parent_hex1[feature]
        parent_color2 = parent_hex2[feature]
        grandparent_colors = [grandparents_hex[parent][feature] for parent in ['Father', 'Mother1', 'Mother2']]
        great_grandparent_colors = [great_grandparents_hex[grandparent][feature] for grandparent in ['Great_Father', 'Great_Mother', 'Great_Father2', 'Great_Mother2']]
        predicted_colors[feature] = blend_colors(hex_to_rgb(parent_color1), hex_to_rgb(parent_color2), *[hex_to_rgb(color) for color in grandparent_colors + great_grandparent_colors])
    return predicted_colors

# Lineage data
lineage_data = {
    'Vermithor': {'HexCode_Scales': '#A77F5B', 'HexCode_Accents': '#C56731', 'HexCode_Eyes': '#C48C55'},
    'Dreamfyre': {'HexCode_Scales': '#7A95C8', 'HexCode_Accents': '#bcc6cc', 'HexCode_Eyes': '#A6C3DC'},
    'Silverwing': {'HexCode_Scales': '#bcc6cc', 'HexCode_Accents': '#bcc6cc', 'HexCode_Eyes': '#A6C3DC'},
    'Cannibal': {'HexCode_Scales': '#181818', 'HexCode_Accents': '#181818', 'HexCode_Eyes': '#59A630'},
    'Quicksilver': {'HexCode_Scales': '#DADADA', 'HexCode_Accents': '#E5B347', 'HexCode_Eyes': '#E5B347'},
    'Vhagar': {'HexCode_Scales': '#9A835A', 'HexCode_Accents': '#4F635A', 'HexCode_Eyes': '#5E7B36'},
    'Balerion': {'HexCode_Scales': '#181818', 'HexCode_Accents': '#181818', 'HexCode_Eyes': '#B83A36'}
}

# Predict colors for Shrykos
predicted_colors_shrykos = predict_offspring_colors(
    {'HexCode_Scales': '#2F37A8', 'HexCode_Accents': '#B87333', 'HexCode_Eyes': '#B87333'},  # Tessarion
    {'HexCode_Scales': '#E5B347', 'HexCode_Accents': '#CC9E9B', 'HexCode_Eyes': '#5E7B36'},  # Sunfyre
    {'Father': lineage_data['Vermithor'], 'Mother1': lineage_data['Dreamfyre'], 'Mother2': lineage_data['Silverwing']},
    {'Great_Father': lineage_data['Cannibal'], 'Great_Mother': lineage_data['Quicksilver'], 'Great_Father2': lineage_data['Balerion'], 'Great_Mother2': lineage_data['Vhagar']}
)
print("Predicted colors for Shrykos:", predicted_colors_shrykos)
Predicted colors for Shrykos: {'HexCode_Scales': '#4A7F80', 'HexCode_Accents': '#AB772D', 'HexCode_Eyes': '#88BB37'}
# Lineage data
lineage_data = {
    'Vermithor': {'HexCode_Scales': '#A77F5B', 'HexCode_Accents': '#C56731', 'HexCode_Eyes': '#C48C55'}, # Grandfather
    'Dreamfyre': {'HexCode_Scales': '#7A95C8', 'HexCode_Accents': '#bcc6cc', 'HexCode_Eyes': '#A6C3DC'}, # Grandmother
    'Silverwing': {'HexCode_Scales': '#bcc6cc', 'HexCode_Accents': '#bcc6cc', 'HexCode_Eyes': '#A6C3DC'}, # Grandmother 2
    'Cannibal': {'HexCode_Scales': '#181818', 'HexCode_Accents': '#181818', 'HexCode_Eyes': '#59A630'}, # Great Grandfather 1
    'Quicksilver': {'HexCode_Scales': '#DADADA', 'HexCode_Accents': '#E5B347', 'HexCode_Eyes': '#E5B347'}, # Great Grandmother 1
    'Vhagar': {'HexCode_Scales': '#9A835A', 'HexCode_Accents': '#4F635A', 'HexCode_Eyes': '#5E7B36'}, # Great Grandmother 2
    'Balerion': {'HexCode_Scales': '#181818', 'HexCode_Accents': '#181818', 'HexCode_Eyes': '#B83A36'} # Great Grandfather 2
}

# Predict colors for Shrykos
predicted_colors_shrykos = predict_offspring_colors(
    {'HexCode_Scales': '#2F37A8', 'HexCode_Accents': '#B87333', 'HexCode_Eyes': '#B87333'},  # Tessarion
    {'HexCode_Scales': '#E5B347', 'HexCode_Accents': '#CC9E9B', 'HexCode_Eyes': '#5E7B36'},  # Sunfyre
    {'Father': lineage_data['Vermithor'], 'Mother1': lineage_data['Dreamfyre'], 'Mother2': lineage_data['Silverwing']},
    {'Great_Father': lineage_data['Cannibal'], 'Great_Mother': lineage_data['Quicksilver'], 'Great_Father2': lineage_data['Balerion'], 'Great_Mother2': lineage_data['Vhagar']}
)

Predicted Colors for Shyrkos

Dataset Colors for Shyrkos

We can see that the model's predicition and the current colors are very close color matches. The model's prediction of green eyes makes more sense, considering that green eyes are dominant and Shrykos' father, Sunfyre, has green eyes. I will be keeping the Jade Green and Copper accents, but updating Shrykos' EyeColor from "Copper" to "Green".

Final Colors for Shyrkos

shrykos_before_after

Shrykos using the dataset colors (left) & Shyrkos using the colors from the predicted model. (right)

tyraxes_vermax_stormcloud = color_df[color_df['Name'].isin(['Tyraxes', 'Vermax', 'Stormcloud'])][['Name', 'ScaleColor', 'WingMembraneColor', 'EyeColor']]

What Color are Tyraxes, Vermax, and Stormcloud?

Like Shrykos and Morghul, not much, if anything at all, is said about the colors of Tyraxes, Vermax, and Stormcloud.

tyraxes_vermax_stormcloud.png

Tyraxes (left), Vermax (middle), Stormcloud (right).

Name ScaleColor WingMembraneColor EyeColor
Stormcloud Dark Grey-Blue Dark Grey Bright Blue
Tyraxes Black Red Red
Vermax Olive Green Pale Orange Yellow

The colors in the dataset are based on the following:

  • Stormcloud: Because of the name, I went with dark greys and blues and blue eyes.
  • Tyraxes: I've noticed a trend in dragons with black and red color combinations being known for their aggressiveness (Balerion, Drogon, Morghul...). With the quote from the previous section mentioning Tyraxes' particularly cunning and aggressive behavior, I felt this coloring made the most sense. Especially with Caraxes, his father, being red himself.
  • Vermax: The illustration of Vermax (seen below) in The Rise of the Dragon, p. 228 isn't a full-body drawing, but does show Vermax's underside being an orangish color and the scale a green color.

Vermax Illustration

Illustration by Tomasz Denmark in "The Rise of The Dragon," p. 228: Vermax in "The Death of Jacaerys and Vermax."

There is also evidence that the color of a dragon's flame correlate with the colors on their body. We know that Silverwing's flames were blue, and Vermithor's were gold. Leaving Vermax's flames as the orange or red flames seen during the Battle of the Gullet.

Flame Colors Table

Dragonflame table from A Wiki from Ice and Fire

We can assume that Stormcloud takes on more of a the grey-ish blue tones, so we will reduce the weight of Vermithor's bronze and green traits in the blending process.

# Blend RGB values from ancestors with some noise
def blend_colors(*args):
    weights = np.array([0.05, 0.05, 0.025, 0.2, 0.2, 0.15, 0.05, 0.15, 0.05]) # Adjusted weights for parents, grandparents, and great-grandparents
    blended_rgb = sum(weight * color for weight, color in zip(weights, args))
    blended_rgb += np.random.normal(0, 20, blended_rgb.shape)  # Add some noise
    return rgb_to_hex(np.clip(blended_rgb, 0, 255))
# Helper functions for color conversions
def hex_to_rgb(hex_code):
    return np.array([int(hex_code[i:i+2], 16) for i in (1, 3, 5)])  # Skip '#' and convert

def rgb_to_hex(rgb):
    clipped_rgb = np.clip(rgb, 0, 255)
    return '#{:02x}{:02x}{:02x}'.format(int(clipped_rgb[0]), int(clipped_rgb[1]), int(clipped_rgb[2]))

# Prepare the feature vector from ancestor hex codes
def prepare_features(parent_hex1, parent_hex2, grandparents_hex, great_grandparents_hex):
    features = {}
    for feature in parent_hex1:
        parent_rgb1 = hex_to_rgb(parent_hex1[feature])
        parent_rgb2 = hex_to_rgb(parent_hex2[feature])
        grandparents_rgb = [hex_to_rgb(grandparents_hex[parent][feature]) for parent in ['Father', 'Mother1', 'Mother2']]
        great_grandparents_rgb = [hex_to_rgb(great_grandparents_hex[grandparent][feature]) for grandparent in ['Great_Father', 'Great_Mother', 'Great_Father2', 'Great_Mother2']]
        feature_vector = np.concatenate([parent_rgb1, parent_rgb2] + grandparents_rgb + great_grandparents_rgb)
        features[feature] = feature_vector
    return features

# Blend RGB values from ancestors with some noise
def blend_colors(*args):
    weights = np.array([0.05, 0.05, 0.025, 0.2, 0.2, 0.15, 0.05, 0.15, 0.05]) # Adjusted weights for parents, grandparents, and great-grandparents
    blended_rgb = sum(weight * color for weight, color in zip(weights, args))
    blended_rgb += np.random.normal(0, 20, blended_rgb.shape)  # Add some noise
    return rgb_to_hex(np.clip(blended_rgb, 0, 255))

# Predict colors for each feature group separately
def predict_offspring_colors(parent_hex1, parent_hex2, grandparents_hex, great_grandparents_hex):
    predicted_colors = {}
    for feature in parent_hex1:
        parent_color1 = parent_hex1[feature]
        parent_color2 = parent_hex2[feature]
        grandparent_colors = [grandparents_hex[parent][feature] for parent in ['Father', 'Mother1', 'Mother2']]
        great_grandparent_colors = [great_grandparents_hex[grandparent][feature] for grandparent in ['Great_Father', 'Great_Mother', 'Great_Father2', 'Great_Mother2']]
        predicted_colors[feature] = blend_colors(hex_to_rgb(parent_color1), hex_to_rgb(parent_color2), *[hex_to_rgb(color) for color in grandparent_colors + great_grandparent_colors])
    return predicted_colors

# Lineage data
lineage_data = {
    'Vermithor': {'HexCode_Scales': '#A77F5B', 'HexCode_Accents': '#C56731', 'HexCode_Eyes': '#C48C55'},
    'Dreamfyre': {'HexCode_Scales': '#7A95C8', 'HexCode_Accents': '#bcc6cc', 'HexCode_Eyes': '#A6C3DC'},
    'Silverwing': {'HexCode_Scales': '#bcc6cc', 'HexCode_Accents': '#bcc6cc', 'HexCode_Eyes': '#A6C3DC'},
    'Cannibal': {'HexCode_Scales': '#181818', 'HexCode_Accents': '#181818', 'HexCode_Eyes': '#59A630'},
    'Quicksilver': {'HexCode_Scales': '#DADADA', 'HexCode_Accents': '#E5B347', 'HexCode_Eyes': '#E5B347'},
    'Vhagar': {'HexCode_Scales': '#9A835A', 'HexCode_Accents': '#4F635A', 'HexCode_Eyes': '#5E7B36'},
    'Balerion': {'HexCode_Scales': '#181818', 'HexCode_Accents': '#181818', 'HexCode_Eyes': '#B83A36'}
}

# Predict colors for Stormcloud
predicted_colors_stormcloud = predict_offspring_colors(
    {'HexCode_Scales': '#F5D21E', 'HexCode_Accents': '#F5D21E', 'HexCode_Eyes': '#5E7B36'},  # Syrax
    {'HexCode_Scales': '#C50016', 'HexCode_Accents': '#C50016', 'HexCode_Eyes': '#F5D21E'},  # Caraxes
    {'Father': lineage_data['Vermithor'], 'Mother1': lineage_data['Dreamfyre'], 'Mother2': lineage_data['Silverwing']},
    {'Great_Father': lineage_data['Cannibal'], 'Great_Mother': lineage_data['Quicksilver'], 'Great_Father2': lineage_data['Balerion'], 'Great_Mother2': lineage_data['Vhagar']}
)
print("Predicted colors for Stormcloud:", predicted_colors_stormcloud)
Predicted colors for Stormcloud: {'HexCode_Scales': '#516078', 'HexCode_Accents': '#65737F', 'HexCode_Eyes': '#79ADA8'}

Predicted Colors for Stormcloud

Dataset Colors for Stormcloud

While not exactly the same, the dark bluish-grey tones seem to be spot on. I think the model's predicted accents are more accurate than the accents in the dataset. I'll be changing the accent color groups from dark grey to grey

# Update 'Stormcloud' Colors
dragon_data.loc[dragon_data['Name'] == 'Stormcloud', ['StandardizedWingColor', 'AccentColorGroup']] = 'Grey'
dragon_data.loc[dragon_data['Name'] == 'Stormcloud', ['WingMembraneColor']] = 'Grey'

Final Colors for Stormcloud

stormcloud_before_after

Stormcloud using the dataset colors (left) & Stormcloud using the colors from the predicted model.(right)

Predicted Colors for Tyraxes

Dataset Colors for Tyraxes

While not exactly the same, I think the model's predicted scale color is more accurate than the scale color in the dataset. I'll be changing the scale color groups from black to grey

# Update 'Tyraxes' Colors
dragon_data.loc[dragon_data['Name'] == 'Tyraxes', ['StandardizedScaleColor', 'ScaleColorGroup']] = 'Grey'
dragon_data.loc[dragon_data['Name'] == 'Tyraxes', ['ScaleColor']] = 'Dark Grey'

Final Colors for Tyraxes

tyraxes_before_after

Tyraxes using the dataset colors (left) & Tyraxes using the colors from the predicted model.(right)

Final Dragon Colors

Shyrkos

Dataset Colors for Shrykos

Predicted Colors for Shrykos

Final Colors for Shrykos

Stormcloud

Dataset Colors for Stormcloud Predicted Colors for Stormcloud

Final Colors for Stormcloud

Tyraxes

Dataset Colors for Tyraxes

Predicted Colors for Tyraxes

Final Colors for Tyraxes

Vermax

Dataset Colors for Vermax

Predicted Colors for Vermax

Final Colors for Vermax


The Tree of the Dragon

Below you'll find the final Targaryen Dragon Family Tree, determined through data and lore.

Family Tree

Final Family Tree showing Targaryen dragon familial connections.

Final Dragon Depictions

Below, you'll you see an image carousel updated with the accurate color predictions of the dragons using dominant and recessive traits determined through machine learning.

Please note that all dragon images were created by me using a personally trained Stable Diffusion 3.0 model. These images are for viewing only and are not available for personal or public use, sharing, or distribution.

Team Black Dragons:

Team Black

All dragons bonded to Targaryens aligned with Queen Rhaenrya Targaryen during The Dance of the Dragons:
Top row: Meleys, Caraxes, Syrax, Seasmoke
Middle row: Vermithor, Silverwing, Vermax, Moondancer
Bottom row: Arrax, Tyraxes, Stormcloud, Morning.

Team Green Dragons:

Team Green

All dragons bonded to Targaryens aligned with King Aegon II Targaryen during The Dance of the Dragons:
Top row: Vhagar, Dreamfyre, Sunfyre
Bottom row: Tessarion, Shrykos, Morghul


Final Conclusions

This project successfully created a detailed and accurate understanding of the Targaryen dragons and their connections. Every dragon's lineage and traits have been carefully examined, resulting in a narrative that stays true to the source material while adding new insights.

Summary of Key Results:

  1. Maternal Lineages:

    • We reassigned certain dragons to more fitting maternal figures. Dreamfyre and Silverwing, for example, were assigned as the offpsring Quicksilver rather than Meraxes.
  2. Dominant and Recessive Traits:

    • We've established which traits are passed down consistently, revealing patterns such as the dominance of green and copper tones from Vhagar, and the rarity of Balerion's red eyes.
  3. Determining Paternity:

    • Using a the dominant & recessive traits in combination with machine learning, we identified likely fathers for several dragons. Dragons like Cannibal and Sheepstealer were found to have played important roles in the family tree.
  4. Predicting Dragon Colors:

    • For dragons with little or no description in the books, we predicted their appearance based on lineage data. These predictions align closely with what we know from the lore and existing illustrations.
  5. Final Family Tree:

    • The resulting family tree visually represents the connections and traits across generations, providing a clear view of the Targaryen dragons' familial connections.

Acknowledgments & References

Unless referenced otherwise, all headers, artwork, and graphics for this project were created by me. Please contact me directly or ensure appropriate credit is given if you use any of them.

  • The headers were created by myself using Canva.

  • I created all portraits of the dragons seen throughout the project through two generative AI models that I personally created and trained using Stable Diffusion 3.0 and hosted on OpenArt.AI.

I created the dataset myself, which can be downloaded through the repository here

The information for the dataset was obtained from the following sources and materials:

Books:

  • Martin, George R. R. The World of Ice and Fire: The Untold History of Westeros and The Game of Thrones. Bantam Books, 2014, p. 78.
  • Martin, George R. R. The Rise of the Dragon: An Illustrated History of the Targaryen Dynasty. Bantam Books, 2022.
  • Martin, George R. R. The World of Ice and Fire: The Untold History of Westeros and The Game of Thrones. Bantam Books, 2014, p. 81.
  • Martin, George R. R. Fire & Blood: 300 Years Before A Game of Thrones (The Targaryen Dynasty: The House of the Dragon). Bantam Books, 2018.
  • Martin, George R. R. The Princess and The Queen. Tor Books, 2013.

Web Sources:


Contact Information

!jupyter nbconvert --to html --template pj --embed-images "The Data of Dragons.ipynb"
pip freeze > requirements.txt
Note: you may need to restart the kernel to use updated packages.