Loading...

Pokemon

This example uses the pokemons and combats datasets to predict the winner of a 1-on-1 Pokemon battle. You can download the two datasets:

pokemons

  • Name: The name of the Pokemon.

  • Generation: Pokemon’s generation.

  • Legendary: True if the Pokemon is legendary.

  • HP: Number of hit points.

  • Attack: Attack stat.

  • Sp_Atk: Special attack stat.

  • Defense: Defense stat.

  • Sp_Def: Special defense stat.

  • Speed: Speed stat.

  • Type_1: Pokemon’s first type.

  • Type_2: Pokemon’s second type.

fights

  • First_pokemon: Pokemon of trainer 1.

  • Second_pokemon: Pokemon of trainer 2.

  • Winner: Winner of the battle.

We will follow the data science cycle (Data Exploration - Data Preparation - Data Modeling - Model Evaluation - Model Deployment) to solve this problem.

Initialization

This example uses the following version of vastorbit:

import vastorbit as vo

vo.__version__

Connect to VAST. This example uses an existing connection called VASTDSN . For details on how to create a connection, see the Connection tutorial. You can skip the below cell if you already have an established connection.

vo.connect("VASTDSN")

Let’s ingest the datasets.

combats = vo.read_csv("fights.csv")
combats
123
first_pokemon
Integer
100%
123
second_pokemon
Integer
100%
123
winner
Integer
100%
1310153153
2349663349
3516130516
4783681783
5657355355
6549100549
7462690690
8112465465
9117370370
1054954549
11478593478
12497280280
13766591766
1467717677
15251259251
1653494494
17746494494
18475723475
19305238305
20288524288
pokemons = vo.read_csv("pokemons.csv")
pokemons
123
id
Integer
100%
Abc
name
Varchar(50)
99%
Abc
type_1
Varchar(50)
100%
Abc
type_2
Varchar(50)
51%
123
hp
Integer
100%
123
attack
Integer
100%
123
defense
Integer
100%
123
sp_atk
Integer
100%
123
sp_def
Integer
100%
123
speed
Integer
100%
123
generation
Integer
100%
0|1
legendary
Boolean
100%
11BulbasaurGrassPoison4549496565451
22IvysaurGrassPoison6062638080601
33VenusaurGrassPoison808283100100801
44Mega VenusaurGrassPoison80100123122120801
55CharmanderFire[null]3952436050651
66CharmeleonFire[null]5864588065801
77CharizardFireFlying788478109851001
88Mega Charizard XFireDragon78130111130851001
99Mega Charizard YFireFlying78104781591151001
1010SquirtleWater[null]4448655064431
1111WartortleWater[null]5963806580581
1212BlastoiseWater[null]798310085105781
1313Mega BlastoiseWater[null]79103120135115781
1414CaterpieBug[null]4530352020451
1515MetapodBug[null]5020552525301
1616ButterfreeBugFlying6045509080701
1717WeedleBugPoison4035302020501
1818KakunaBugPoison4525502525351
1919BeedrillBugPoison6590404580751
2020Mega BeedrillBugPoison651504015801451

Data Exploration and Preparation

The table combats will be joined to the table pokemons to predict the winner.

The pokemons table contains the information on each Pokemon. Let’s describe this table.

pokemons.describe(method = "categorical", unique = True)
dtypecounttoptop_percentunique
"id"integer800130.125800.0
"name"varchar(50)799Spearow0.125799.0
"type_1"varchar(50)800Water14.018.0
"type_2"varchar(50)414[null]48.2518.0
"hp"integer800608.37594.0
"attack"integer8001005.0111.0
"defense"integer800706.75103.0
"sp_atk"integer800606.375105.0
"sp_def"integer800806.592.0
"speed"integer800505.75108.0
"generation"integer800120.756.0
"legendary"boolean80091.8752.0

The pokemons’s Name, Generation, and whether or not it’s Legendary will never influence the outcome of the battle, so we can drop these columns.

pokemons.drop(
    [
        "Generation",
        "Legendary",
        "Name",
    ]
)
123
id
Integer
100%
Abc
type_1
Varchar(50)
100%
Abc
type_2
Varchar(50)
51%
123
hp
Integer
100%
123
attack
Integer
100%
123
defense
Integer
100%
123
sp_atk
Integer
100%
123
sp_def
Integer
100%
123
speed
Integer
100%
11GrassPoison454949656545
22GrassPoison606263808060
33GrassPoison80828310010080
44GrassPoison8010012312212080
55Fire[null]395243605065
66Fire[null]586458806580
77FireFlying78847810985100
88FireDragon7813011113085100
99FireFlying7810478159115100
1010Water[null]444865506443
1111Water[null]596380658058
1212Water[null]79831008510578
1313Water[null]7910312013511578
1414Bug[null]453035202045
1515Bug[null]502055252530
1616BugFlying604550908070
1717BugPoison403530202050
1818BugPoison452550252535
1919BugPoison659040458075
2020BugPoison65150401580145

The ID will be the key to join the data. By joining the data, we will be able to create more relevant features.

fights = pokemons.join(
    combats,
    on = {"ID": "First_Pokemon"},
    how = "inner",
    expr1 = [
        "Sp_Atk AS Sp_Atk_1",
        "Speed AS Speed_1",
        "Sp_Def AS Sp_Def_1",
        "Defense AS Defense_1",
        "Type_1 AS Type_1_1",
        "Type_2 AS Type_2_1",
        "HP AS HP_1",
        "Attack AS Attack_1",
    ],
    expr2 = [
        "First_Pokemon",
        "Second_Pokemon",
        "Winner",
    ]).join(pokemons,
    on = {"Second_Pokemon": "ID"},
    how = "inner",
    expr2 = [
        "Sp_Atk AS Sp_Atk_2",
        "Speed AS Speed_2",
        "Sp_Def AS Sp_Def_2",
        "Defense AS Defense_2",
        "Type_1 AS Type_1_2",
        "Type_2 AS Type_2_2",
        "HP AS HP_2",
        "Attack AS Attack_2",
    ],
    expr1 =
        [
            "Sp_Atk_1",
            "Speed_1",
            "Sp_Def_1",
            "Defense_1",
            "Type_1_1",
            "Type_2_1",
            "HP_1",
            "Attack_1",
            "Winner",
            "Second_pokemon",
        ]
)

Features engineering is the key. Here, we can create features that describe the stat differences between the first and second Pokemon. We can also change winner to a binary value: 1 if the first pokemons won and 0 otherwise.

import vastorbit.sql.functions as fun

fights["Sp_Atk_diff"] = fights["Sp_Atk_1"] - fights["Sp_Atk_2"]
fights["Speed_diff"] = fights["Speed_1"] - fights["Speed_2"]
fights["Sp_Def_diff"] = fights["Sp_Def_1"] - fights["Sp_Def_2"]
fights["Defense_diff"] = fights["Defense_1"] - fights["Defense_2"]
fights["HP_diff"] = fights["HP_1"] - fights["HP_2"]
fights["Attack_diff"] = fights["Attack_1"] - fights["Attack_2"]
fights["Winner"] = fun.case_when(fights["Winner"] == fights["Second_pokemon"], 0, 1)
fights = fights[
    [
        "Sp_Atk_diff",
        "Speed_diff",
        "Sp_Def_diff",
        "Defense_diff",
        "HP_diff",
        "Attack_diff",
        "Type_1_1",
        "Type_1_2",
        "Type_2_1",
        "Type_2_2",
        "Winner",
    ]
]

Missing values can not be handled by most machine learning models. Let’s see which features we should impute.

fights.count()
count
"Sp_Atk_diff"50000.0
"Speed_diff"50000.0
"Sp_Def_diff"50000.0
"Defense_diff"50000.0
"HP_diff"50000.0
"Attack_diff"50000.0
"Type_1_1"50000.0
"Type_1_2"50000.0
"Type_2_1"25969.0
"Type_2_2"26015.0
"Winner"50000.0

In terms of missing values, our only concern is the Pokemon’s second type (Type_2_1 and Type_2_2). Since some Pokemon only have one type, these features are MNAR (missing values not at random). We can impute the missing values by creating another category.

fights["Type_2_1"].fillna("No")
fights["Type_2_2"].fillna("No")
123
Sp_Atk_diff
Integer
100%
123
Speed_diff
Integer
100%
123
Sp_Def_diff
Integer
100%
123
Defense_diff
Integer
100%
123
HP_diff
Integer
100%
123
Attack_diff
Integer
100%
Abc
Type_1_1
Varchar(50)
100%
Abc
Type_1_2
Varchar(50)
100%
Abc
Type_2_1
Varchar(50)
100%
Abc
Type_2_2
Varchar(50)
100%
123
Winner
Integer
100%
123
Type_1_1_Bug
Bool
100%
123
Type_1_1_Dark
Bool
100%
123
Type_1_1_Dragon
Bool
100%
123
Type_1_1_Electric
Bool
100%
123
Type_1_1_Fairy
Bool
100%
123
Type_1_1_Fighting
Bool
100%
123
Type_1_1_Fire
Bool
100%
123
Type_1_1_Flying
Bool
100%
123
Type_1_1_Ghost
Bool
100%
123
Type_1_1_Grass
Bool
100%
123
Type_1_1_Ground
Bool
100%
123
Type_1_1_Ice
Bool
100%
123
Type_1_1_Normal
Bool
100%
123
Type_1_1_Poison
Bool
100%
...
123
Type_2_1_Ice
Bool
100%
123
Type_2_1_No
Bool
100%
123
Type_2_1_Normal
Bool
100%
123
Type_2_1_Poison
Bool
100%
123
Type_2_1_Psychic
Bool
100%
123
Type_2_1_Rock
Bool
100%
123
Type_2_1_Steel
Bool
100%
123
Type_2_2_Bug
Bool
100%
123
Type_2_2_Dark
Bool
100%
123
Type_2_2_Dragon
Bool
100%
123
Type_2_2_Electric
Bool
100%
123
Type_2_2_Fairy
Bool
100%
123
Type_2_2_Fighting
Bool
100%
123
Type_2_2_Fire
Bool
100%
123
Type_2_2_Flying
Bool
100%
123
Type_2_2_Ghost
Bool
100%
123
Type_2_2_Grass
Bool
100%
123
Type_2_2_Ground
Bool
100%
123
Type_2_2_Ice
Bool
100%
123
Type_2_2_No
Bool
100%
123
Type_2_2_Normal
Bool
100%
123
Type_2_2_Poison
Bool
100%
123
Type_2_2_Psychic
Bool
100%
123
Type_2_2_Rock
Bool
100%
123
Type_2_2_Steel
Bool
100%
1-45-59-155-15-21GrassFireNoNo000000000010000...0100000000000000000100000
265493082918FireGhostFlyingGrass100000010000000...0000000000000000100000000
3-50-35-58-38-38-23NormalSteelFlyingPsychic000000000000010...0000000000000000000000100
4-61-14-44-54-26-61WaterDragonNoFairy000000000000000...0100000000010000000000000
5-15-205075-100SteelWaterNoNo000000000000000...0100000000000000000100000
6-30351055-25NormalGrassNoPoison100000000000010...0100000000000000000001000
7-78-45-18-7036-160NormalGhostNoNo000000000000010...0100000000000000000100000
815-502535-17-45GhostNormalNoFlying000000000100000...0100000000000010000000000
90433035-700GrassNormalNoFairy100000000010000...0100000000010000000000000
10-52845-21-2-10WaterFairyPoisonNo100000000000000...0001000000000000000100000
11-10-510-30155FightingDragonNoNo000000100000000...0100000000000000000100000
1242421713341PoisonWaterFightingNo100000000000001...0000000000000000000100000
13-2070-530525NormalNormalNoNo100000000000010...0100000000000000000100000
1418-113822294BugWaterFlyingNo010000000000000...0000000000000000000100000
15106235253367DragonWaterNoNo100100000000000...0100000000000000000100000
1622110-37515FightingGhostPsychicGrass000000100000000...0000100000000000100000000
1785-30-55550-5FireBugPsychicFlying000000010000000...0000100000000010000000000
183030-30-308035WaterNormalNoNo100000000000000...0100000000000000000100000
19-6-392855-27GroundFireNoFlying000000000001000...0100000000000010000000000
20-55-32-855-125-10SteelWaterGhostNo000000000000000...0000000000000000000100000

Let’s use a one hot encoder to get numerical dummies out of the different types.

fights["Type_1_1"].one_hot_encode()
fights["Type_1_2"].one_hot_encode()
fights["Type_2_1"].one_hot_encode()
fights["Type_2_2"].one_hot_encode()
123
Sp_Atk_diff
Integer
100%
123
Speed_diff
Integer
100%
123
Sp_Def_diff
Integer
100%
123
Defense_diff
Integer
100%
123
HP_diff
Integer
100%
123
Attack_diff
Integer
100%
Abc
Type_1_1
Varchar(50)
100%
Abc
Type_1_2
Varchar(50)
100%
Abc
Type_2_1
Varchar(50)
100%
Abc
Type_2_2
Varchar(50)
100%
123
Winner
Integer
100%
123
Type_1_1_Bug
Bool
100%
123
Type_1_1_Dark
Bool
100%
123
Type_1_1_Dragon
Bool
100%
123
Type_1_1_Electric
Bool
100%
123
Type_1_1_Fairy
Bool
100%
123
Type_1_1_Fighting
Bool
100%
123
Type_1_1_Fire
Bool
100%
123
Type_1_1_Flying
Bool
100%
123
Type_1_1_Ghost
Bool
100%
123
Type_1_1_Grass
Bool
100%
123
Type_1_1_Ground
Bool
100%
123
Type_1_1_Ice
Bool
100%
123
Type_1_1_Normal
Bool
100%
123
Type_1_1_Poison
Bool
100%
...
123
Type_2_1_Ice
Bool
100%
123
Type_2_1_No
Bool
100%
123
Type_2_1_Normal
Bool
100%
123
Type_2_1_Poison
Bool
100%
123
Type_2_1_Psychic
Bool
100%
123
Type_2_1_Rock
Bool
100%
123
Type_2_1_Steel
Bool
100%
123
Type_2_2_Bug
Bool
100%
123
Type_2_2_Dark
Bool
100%
123
Type_2_2_Dragon
Bool
100%
123
Type_2_2_Electric
Bool
100%
123
Type_2_2_Fairy
Bool
100%
123
Type_2_2_Fighting
Bool
100%
123
Type_2_2_Fire
Bool
100%
123
Type_2_2_Flying
Bool
100%
123
Type_2_2_Ghost
Bool
100%
123
Type_2_2_Grass
Bool
100%
123
Type_2_2_Ground
Bool
100%
123
Type_2_2_Ice
Bool
100%
123
Type_2_2_No
Bool
100%
123
Type_2_2_Normal
Bool
100%
123
Type_2_2_Poison
Bool
100%
123
Type_2_2_Psychic
Bool
100%
123
Type_2_2_Rock
Bool
100%
123
Type_2_2_Steel
Bool
100%
1-45-59-155-15-21GrassFireNoNo000000000010000...0100000000000000000100000
265493082918FireGhostFlyingGrass100000010000000...0000000000000000100000000
3-50-35-58-38-38-23NormalSteelFlyingPsychic000000000000010...0000000000000000000000100
4-61-14-44-54-26-61WaterDragonNoFairy000000000000000...0100000000010000000000000
5-15-205075-100SteelWaterNoNo000000000000000...0100000000000000000100000
6-30351055-25NormalGrassNoPoison100000000000010...0100000000000000000001000
7-78-45-18-7036-160NormalGhostNoNo000000000000010...0100000000000000000100000
815-502535-17-45GhostNormalNoFlying000000000100000...0100000000000010000000000
90433035-700GrassNormalNoFairy100000000010000...0100000000010000000000000
10-52845-21-2-10WaterFairyPoisonNo100000000000000...0001000000000000000100000
11-10-510-30155FightingDragonNoNo000000100000000...0100000000000000000100000
1242421713341PoisonWaterFightingNo100000000000001...0000000000000000000100000
13-2070-530525NormalNormalNoNo100000000000010...0100000000000000000100000
1418-113822294BugWaterFlyingNo010000000000000...0000000000000000000100000
15106235253367DragonWaterNoNo100100000000000...0100000000000000000100000
1622110-37515FightingGhostPsychicGrass000000100000000...0000100000000000100000000
1785-30-55550-5FireBugPsychicFlying000000010000000...0000100000000010000000000
183030-30-308035WaterNormalNoNo100000000000000...0100000000000000000100000
19-6-392855-27GroundFireNoFlying000000000001000...0100000000000010000000000
20-55-32-855-125-10SteelWaterGhostNo000000000000000...0000000000000000000100000

Let’s use the current_relation method to see how our data preparation so far on the VastFrame generates SQL code.

print(fights.current_relation())

vastorbit will remember your modifications and always generate an up-to-date SQL query.

Let’s look at the correlations between all the variables.

fights.corr(method = "spearman")

Many variables are correlated to the response column. We have enough information to create our predictive model.

Machine Learning

Let’s create a LogisticRegression to see the importance of the features in the final result.

from vastorbit.machine_learning.vast import LogisticRegression
from vastorbit.machine_learning.model_selection import cross_validate

predictors = fights.get_columns(exclude_columns = ["Winner", "Type_1_1", "Type_1_2", "Type_2_1", "Type_2_2"])
model = LogisticRegression(max_iter=1000)
cross_validate(model, fights, predictors, "Winner")
aucprc_aucaccuracylog_lossprecisionrecallf1_scoremccinformednessmarkednesscsitime
1-fold0.93262326874514640.32468453374505490.89155487264346080.34944335602442520.88873609201150140.88266699776508570.88569114807201140.78255644010810790.78229483130517830.78281813639616840.79483452593917711.2829837799072266
2-fold0.93226331249581580.328045352685590650.88957706600601660.35189329099969290.88579181055747410.88361220472440950.88470066518847020.77876087409612960.77868302625415440.77883872972084320.79324055666003981.3019630908966064
3-fold0.93264993669830120.323640789863706150.89070206417828660.35326999712823890.89042345276872970.87750385208012330.88391644570911210.7807394863884970.78010952666032020.78136995482634290.79198053076833931.3493530750274658
avg0.93251217264642120.325456892098117230.89061133427592140.351535548050785630.88831711844590180.88126101818987290.88476941965653120.78068560019757830.7803624614065510.78100894031445140.79335187112251881.3114333152770996
std0.000176307167278860740.00187926341636657680.00080998095046199230.00158256764941634430.00191392816475689350.00268459518129847550.00072614851643397270.00155000174817105320.00148532059862251440.00164451978406113270.00116779430008289970.02791038869969411

We have an excellent model with an average AUC of more than 99%. Let’s create a model with the entire dataset and look at the importance of each feature.

model.fit(
    fights,
    predictors,
    "Winner",
)
model.features_importance()

Based on our model, it seems that a Pokemon’s speed and attack stats are the strongest predictors for the winner of a battle.

Conclusion

We’ve solved our problem in a pandas-like way, all without ever loading data into memory!