Dziś w całej Polsce obchodzimy Międzynarodowy Dzień Dziecka. Z tej okazji publikuję nietypowy wpis w którym wykonuję grupowanie Pokemonów. 😉
- Wstęp.
- Cel projekty.
- Założenia dotyczące projektu.
- Opis zbioru danych.
- Wczytanie danych i niezbędnych bibliotek.
- Przygotowanie danych do analizy.
- Eksploracyjna analiza danych.
- Przygotowanie danych do modelowania.
- Modelowanie.
- Przygotowanie danych do analizy.
- Analiza segmentów.
- Opis uzyskanych segmentów
- Podsumowanie.
1. Wstęp.¶
Podejdę do grupowania pokemonów w taki sam sposób, w jaki podszedłbym do grupowania np. klientów banku. Postaram się zachować pełną powagę i profesjonalizm, dlatego proszę Cię o traktowanie tego wpisu z przymrużeniem oka. 😉
2. Cel projektu.¶
Celem projektu jest wykonanie grupowania zbioru pokemonów, który powinien zostać podzielony na możliwie różne segmenty. Dodatkowym celem jest opisanie każdego z segmentów.
Jakość grupowania (która również posłuży mi do wybrania optymalnej liczby grup) będę mierzył za pomocą "miary odmienności", która jest używana w algorytmie k-ptototypes, którego użyję.
3. Założenia dotyczące projektu.¶
Przed przystąpieniem do prac nam zbiorem muszę wymienić kilka założeń, które przyjąłem:
- Użyję algorytmu k-prototype, który do tej pory nie był opisywany szerzej na moim blogu. Wkrótce planuję to nadrobić i zamieszczę link w tym miejscu.
- Jeśli chodzi o dobór parametrów modelu, to dobiorę jest w sposób ekspercki, gdyż nie będą one najistotniejszym elementem projektu.
- Dużo ważniejsze od parametrów będzie tu odpowiednie przygotownia danych, dlatego właśnie na tym się skupię. Wykonam, m.in: usunięcie wartości odstających, standaryzacja zmiennych i usunięcie skośności rozkładów.
4. Opis zbioru danych.¶
Główne informacje o zbiorze:
- Pokemony.
- 13 zmiennych, 800 obserwacji/pokemonów.
5. Wczytanie danych i niezbędnych bibliotek.¶
Cały projekt wykonuję w Pythonie, korzystając z Jupyter-a. Pierwszym krokiem jest więc zatem wczytanie niezbędnych bibliotek.
5.1. Wczytanie bibliotek.¶
import pandas as pd
import numpy as np
import seaborn as sns
import matplotlib.pyplot as plt
import scipy
import statsmodels.formula.api as sm
from sklearn.preprocessing import StandardScaler
from kmodes.kprototypes import KPrototypes
5.2. Wczytanie zbioru.¶
Ustawiam formatowanie zmiennych ciągłych w Pandas z precyzją do trzech miejsc po przecinku.
pd.set_option('float_format', '{:.3f}'.format)
Wczytuję teraz zbiór z pliku csv. To, co robię dodatkowo przy wczytywaniu, to parametrami wskazuję Pandas-owi:
- nazwy poszczególnych zmiennych,
- typy niektórych zmiennych.
df = pd.read_csv('data/Pokemon.csv', dtype={'Name':'object', 'Type 1':'object', 'Type 2':'object', 'Legendary':'int64'})
df.columns = ['#', 'nazwa_pokemona', 'typ_1', 'typ_2', 'suma_sil', 'poziom_zycia', 'atak', 'obrona', 'specjalny_atak', 'specjalna_obrona', 'szybkosc', 'generacja', 'legendarny']
Mając wczytany zbiór, sprawdzam jego rozmiar i podglądam, jak wyglądają poszczególne obserwacje, by sprawdzić, czy wszystko wczytało się prawidłowo.
print(str(df.shape[0]) + ' wierszy.')
print(str(df.shape[1]) + ' kolumn.')
df.head()
5.3. Wstępne przygotowanie zbioru.¶
Usuwam zbędną zmienną '#'.
df.drop('#', axis=1, inplace=True)
Sprawdzam jeszcze typy poszczególnych zmiennych.
df.dtypes
Zmiana wartości zmiennej "typ_1" i "typ_2" na podstawie wiedzy eksperckiej. 😉
df.typ_1.unique()
df.typ_2.unique()
df.typ_1.replace(df.typ_1.unique(),['Trawiasty', 'Ognisty', 'Wodny', 'Robaczy', 'Normalny', 'Trujący', 'Elektryczny', 'Ziemny', 'Wróżka', 'Walczący', 'Psychiczny', 'Kamienny', 'Duch', 'Lodowy','Smoczy','Mroczny','Stalowy','Powietrzny'], inplace=True)
df.typ_2.replace(df.typ_2.unique(), ['Trujący', 'brak', 'Powietrzny', 'Smoczy', 'Ziemny', 'Wróżka', 'Trawiasty', 'Walczący', 'Psychiczny', 'Stalowy', 'Lodowy', 'Kamienny', 'Mroczny', 'Wodny', 'Elektryczny', 'Ognisty', 'Duch', 'Robaczy', 'Normalny'], inplace=True)
df.legendarny.replace([0, 1], ['legendarny_nie', 'legendarny_tak'], inplace = True)
df.head()
Teraz zbiór wygląda zdecydowanie lepiej. 🙂
6. Przygotowanie danych do analizy.¶
Sprawdzam jeszcze raz typy zmiennych. Dla zmiennych nominalnych potrzebujemy 'objectów'.
df.dtypes
Buduję prosty data frame z podstawowymi informacjami o zbiorze.
summary = pd.DataFrame(df.dtypes, columns=['Dtype'])
summary['Nulls'] = pd.DataFrame(df.isnull().any())
summary['Sum_of_nulls'] = pd.DataFrame(df.isnull().sum())
summary['Per_of_nulls'] = round((df.apply(pd.isnull).mean()*100),2)
summary.Dtype = summary.Dtype.astype(str)
print(summary)
A więc żadna ze zmiennych nie zawiera brakujących wartości (uzupełniłem wszystko w jednym z poprzednich kroków).
7. Eksploracyjna analiza danych.¶
7.1. Analiza pojedynczych zmiennych.¶
7.1.1. Analiza zmiennych numerycznych.¶
W tym punkcie zbadam zmienne numeryczne. Skupię się na analizie wykresów pod kątem ewentualnych anomalii i wykrycia wartości odstających. Dodatkowo zbadam rozkład zmiennych z użyciem biblioteki scipy.
stats = df.select_dtypes(['float', 'int']).describe()
stats = stats.transpose()
stats = stats[['count','std','min','25%','50%','75%','max','mean']]
stats
Zmienna „atak”.
# Histogram
plt.figure(figsize=(13,7))
sns.set(font_scale=1.4, style="whitegrid")
sns.distplot(df['atak'], kde = False, bins = 30, color = '#eb6c6a').set(title = 'Histogram - "atak"', xlabel = 'atak', ylabel = 'liczba obserwacji')
plt.show()
# Wykres gęstości
plt.figure(figsize=(13,7))
sns.set(font_scale=1.4, style="whitegrid")
sns.kdeplot(df['atak'], shade = True, color = '#eb6c6a').set(title = 'Wykres gęstości - "atak"', xlabel = 'atak', ylabel = '')
plt.show()
# Wykres pudełkowy
plt.figure(figsize=(13,7))
sns.set(font_scale=1.4, style="whitegrid")
sns.boxplot(df['atak'], color = '#eb6c6a').set(title = 'Wykres pudełkowy - "atak"', xlabel = 'atak')
plt.show()
# Test na normalność rozkładu
# Zakładany poziom istotności alfa = 0.05.
if(scipy.stats.normaltest(df['atak'])[1] < 0.05):
print('Odrzucam hipotezę zerową i przyjmuję hipotezę alternatywną: zmienna nie pochodzi z rozkładu normalnego.')
else:
print('Przyjmuję hipotezę zerową. Zmienna pochodzi z rozkładu normalnego.')
Rozkład niemalże normalny. Niewiele wartości odstających.
Zmienna „obrona”.
# Histogram
plt.figure(figsize=(13,7))
sns.set(font_scale=1.4, style="whitegrid")
sns.distplot(df['obrona'], kde = False, bins = 30, color = '#eb6c6a').set(title = 'Histogram - "obrona"', xlabel = 'obrona', ylabel = 'liczba obserwacji')
plt.show()
# Wykres gęstości
plt.figure(figsize=(13,7))
sns.set(font_scale=1.4, style="whitegrid")
sns.kdeplot(df['obrona'], shade = True, color = '#eb6c6a').set(title = 'Wykres gęstości - "obrona"', xlabel = 'obrona', ylabel = '')
plt.show()
# Wykres pudełkowy
plt.figure(figsize=(13,7))
sns.set(font_scale=1.4, style="whitegrid")
sns.boxplot(df['obrona'], color = '#eb6c6a').set(title = 'Wykres pudełkowy - "obrona"', xlabel = 'obrona')
plt.show()
# Test na normalność rozkładu
# Zakładany poziom istotności alfa = 0.05.
if(scipy.stats.normaltest(df['obrona'])[1] < 0.05):
print('Odrzucam hipotezę zerową i przyjmuję hipotezę alternatywną: zmienna nie pochodzi z rozkładu normalnego.')
else:
print('Przyjmuję hipotezę zerową. Zmienna pochodzi z rozkładu normalnego.')
Rozkład zdecydowanie prawoskośny. Niewiele outlier-ów.
Zmienna „poziom_zycia”.
# Histogram
plt.figure(figsize=(13,7))
sns.set(font_scale=1.4, style="whitegrid")
sns.distplot(df['poziom_zycia'], kde = False, bins = 30, color = '#eb6c6a').set(title = 'Histogram - "poziom_zycia"', xlabel = 'poziom_zycia', ylabel = 'liczba obserwacji')
plt.show()
# Wykres gęstości
plt.figure(figsize=(13,7))
sns.set(font_scale=1.4, style="whitegrid")
sns.kdeplot(df['poziom_zycia'], shade = True, color = '#eb6c6a').set(title = 'Wykres gęstości - "poziom_zycia"', xlabel = 'poziom_zycia', ylabel = '')
plt.show()
# Wykres pudełkowy
plt.figure(figsize=(13,7))
sns.set(font_scale=1.4, style="whitegrid")
sns.boxplot(df['poziom_zycia'], color = '#eb6c6a').set(title = 'Wykres pudełkowy - "poziom_zycia"', xlabel = 'poziom_zycia', ylabel = 'liczba obserwacji')
plt.show()
# Test na normalność rozkładu
# Zakładany poziom istotności alfa = 0.05.
if(scipy.stats.normaltest(df['poziom_zycia'])[1] < 0.05):
print('Odrzucam hipotezę zerową i przyjmuję hipotezę alternatywną: zmienna nie pochodzi z rozkładu normalnego.')
else:
print('Przyjmuję hipotezę zerową. Zmienna pochodzi z rozkładu normalnego.')
Rozkład prawostronnie skośny i nieco więcej wartości odstających niż w przypadku poprzednich zmiennych.
7.1.2. Zmienne kategoryczne.¶
Sprawdzam zatem liczbę unikalnych kategorii wchodzących w skład wybranych zmiennych.
df.select_dtypes(exclude = ['float', 'int']).describe()
Najpopularniejszym typem pokemona jest pokemon wodny i nielegendarny (cokolwiek to oznacza).
Zmienna „typ_1”.
print('Rozkład zmiennej "typ_1"')
print('-------------------------')
print(df['typ_1'].value_counts(normalize = True))
Zmienna „typ_2”.
print('Rozkład zmiennej "typ_2"')
print('-------------------------')
print(df['typ_2'].value_counts(normalize = True))
Zmienna "legendarny".
print('Rozkład zmiennej "legendarny"')
print(df['legendarny'].value_counts(normalize = True))
plt.figure(figsize=(10,7))
sns.set(font_scale=1.4, style="whitegrid")
sns.countplot(df['legendarny'], palette = ['#eb6c6a', '#f0918f', '#f2a3a2', '#f5b5b4', '#f7c8c7'], order = df['legendarny'].value_counts().index).set(title = 'Wykres częstości - "legendarny"', xlabel = 'legendarny', ylabel = 'liczba obserwacji')
plt.show()
A więc pokemon legendarny jest rzadkością.
7.2. Analiza zależności pomiędzy zmiennymi.¶
7.2.1. Analiza korelacji pomiędzy zmiennymi numerycznymi.¶
corr_num = pd.DataFrame(scipy.stats.spearmanr(df.select_dtypes(include = ['float', 'int']))[0],
columns = df.select_dtypes(include = ['float', 'int']).columns,
index = df.select_dtypes(include = ['float', 'int']).columns)
plt.figure(figsize=(15,6))
sns.set(font_scale=1)
sns.heatmap(corr_num.abs(), cmap="Reds", linewidths=.5).set(title='Heatmap-a współczynnika korelacji rang Spearmana')
plt.show()
7.2.2. Analiza zależności pomiędzy zmiennymi kategorycznymi.¶
def CramersV(tab):
a = scipy.stats.chi2_contingency(tab)[0]/sum(tab.sum())
b = min(tab.shape[0]-1, tab.shape[1]-1,)
return(np.sqrt(a/b))
def CalculateCrammersV(tab):
ret = []
for m in tab:
row = []
for n in tab:
cross_tab = pd.crosstab(tab[m].values,tab[n].values)
row.append(CramersV(cross_tab))
ret.append(row)
return pd.DataFrame(ret, columns=tab.columns, index=tab.columns)
crammer = CalculateCrammersV(df[['typ_1', 'typ_2', 'legendarny']])
plt.figure(figsize=(15,6))
sns.set(font_scale=1.4)
sns.heatmap(crammer, cmap="Reds", linewidths=.5).set(title='Heatmap-a współczynnika zależności Crammera')
plt.show()
crammer.drop('legendarny', axis = 0)
7.3. Analiza zależności pomiędzy zmiennymi numerycznymi a kategorycznymi.¶
W przypadku zmiennych numerycznych i kategorycznych nie mówimy już o korelacji, lecz o poziomie zależności. Miarą, która jest najbliższa współczynnikowi korelacji, w tym przypadku jest współczynnik korelacji wielorakiej (współczynnik R w modelach regresji) – użyję jej do badania zależności pomiędzy zmiennymi kategorycznymi (nominalnymi i porządkowymi) i numerycznymi (liczby całkowite i zmiennoprzecinkowe). Analogiczną miarą może być Eta-squared.
# dzielę zmienne na dwie grupy
cat_cols = ['typ_1', 'typ_2', 'legendarny', 'nazwa_pokemona']
num_cols = df.drop(cat_cols, axis = 1).columns
cat_cols.remove('nazwa_pokemona')
# w pętli buduję kolejne modele regresji liniowej
cols = []
for cat in cat_cols:
rows = []
for num in num_cols:
formula = num + '~' +cat
model = sm.ols(formula=formula,data=df)
rows.append(np.sqrt(model.fit().rsquared))
cols.append(rows)
corr_num_cat = pd.DataFrame(cols, index = cat_cols, columns = num_cols)
# wykres zależności
plt.figure(figsize=(15,6))
sns.set(font_scale=1.4)
sns.heatmap(corr_num_cat, cmap="Reds", linewidths=.5).set(title='Heatmap-a współczynnika korelacji wielorakiej')
plt.show()
corr_num_cat
8. Przygotowanie danych do modelowania.¶
8.1. Usunięcie zbędnych zmiennych.¶
Usuwam zbędne zmienne, z których już nie będę korzystać.
df.drop(df.drop(['nazwa_pokemona', 'typ_1', 'typ_2', 'legendarny', 'atak', 'obrona', 'poziom_zycia'], axis = 1).columns, axis = 1, inplace = True)
8.2. Zmiana kodowania zmiennych.¶
Nie wykonuję, gdyż k-prototypów świetnie radzi sobie ze zmiennymi kategorycznymi.
8.3. Usunięcie skośności zmiennych.¶
df = pd.concat([df.drop(['poziom_zycia', 'atak', 'obrona'], axis = 1), np.log(df.select_dtypes(include = ['float', 'int']))], axis = 1)
8.4. Usunięcie odstających wartości.¶
Zanim przystąpię do modelowania, muszę usunąć wartości odstające. Reguła 1.5 IQR nie wchodzi w grę – sprawdziłem to i usuwała ona ok. xx% obserwacji. Nie mogę również zastosować reguły 3sigma, gdyż zakłada ona normalny rozkład zmiennych. Usuwam zatem 5% skrajnych wartości.
q1 = df.quantile(0.25)
q3 = df.quantile(0.75)
iqr = q3 - q1
low_boundary = (q1 - 1.5 * iqr)
upp_boundary = (q3 + 1.5 * iqr)
num_of_outliers_L = (df[iqr.index] < low_boundary).sum()
num_of_outliers_U = (df[iqr.index] > upp_boundary).sum()
outliers = pd.DataFrame({'lower_boundary':low_boundary, 'upper_boundary':upp_boundary,'num_of_outliers__lower_boundary':num_of_outliers_L, 'num_of_outliers__upper_boundary':num_of_outliers_U})
outliers
W pętli usuwam wszystkie obserwacje, które nie spełniają dwóch warunków.
for row in outliers.iterrows():
df = df[(df[row[0]] >= row[1]['lower_boundary']) & (df[row[0]] <= row[1]['upper_boundary'])]
8.5. Standaryzacja zmiennych.¶
scaler = StandardScaler()
scaler.fit(df[['poziom_zycia', 'atak', 'obrona']])
df_std = scaler.transform(df[['poziom_zycia', 'atak', 'obrona']].values)
df = pd.concat([df.drop(['poziom_zycia', 'atak', 'obrona'], axis = 1), pd.DataFrame(df_std, columns = ['poziom_zycia', 'atak', 'obrona'], index = df.index)], axis = 1)
Po wykonaniu kilku transformacji ponownie sprawdzam rozkłady zmiennych i podstawowe statystyki zbioru.
plt.figure(figsize=(10,7))
sns.set(font_scale=1.4, style="whitegrid")
sns.distplot(df.atak, color = '#eb6c6a').set(title = 'Wykres gęstości - zmienna atak')
plt.show()
plt.figure(figsize=(10,7))
sns.set(font_scale=1.4, style="whitegrid")
sns.distplot(df.obrona, color = '#eb6c6a').set(title = 'Wykres gęstości - zmienna obrona')
plt.show()
plt.figure(figsize=(10,7))
sns.set(font_scale=1.4, style="whitegrid")
sns.distplot(df.poziom_zycia, color = '#eb6c6a').set(title = 'Wykres gęstości - zmienna poziom_zycia')
plt.show()
df.agg(['mean', 'std', 'max', 'min']).round(2)
8.6. Wnioski przed modelowaniem.¶
Ok, mam zatem odpowiednio przygotowany zbiór. Mogę zatem przejść dalej. Mój plan na dalszą część wygląda następująco:
- Zbadam, jaka jest "optymalna" liczba segmentów dla zbioru pokemonów.
- Zbuduję model.
- Opiszę uzyskane wyniki - wykonam krótką EDA dla powstałych segmentów i na podstawie uzyskanych danych postaram się słownie opisać każdy segment.
9. Modelowanie.¶
res = []
for n in range(1, 20):
km = KPrototypes(n_clusters=n, init='Huang', n_init=10)
km.fit_predict(df.drop('nazwa_pokemona', axis = 1), categorical = [0, 1, 2])
res.append([n, km.cost_])
res = pd.DataFrame(res, columns=[0, 'wspolcz_odm']).set_index(0)
diff = [0]
for n in range(0, 18):
diff.append(((res.iloc[n,]-res.loc[n+2])/res.iloc[n,]*100).values[0])
res = res.assign(zysk_proc = diff)
res
plt.figure(figsize=(10,7))
sns.set(font_scale=1.4, style="whitegrid")
sns.lineplot(data = res.drop('zysk_proc', axis = 1), palette = ['#eb6c6a']).set(title = "Miara odmienności grup vs liczba grup")
plt.show()
Na podstawie wykresu i powyższej tabeli decyduję się na podział zbioru na 5 segmentów.
km = KPrototypes(n_clusters=5, init='Huang', n_init=10)
clusters = km.fit_predict(df.drop('nazwa_pokemona', axis = 1), categorical = [0, 1, 2])
10. Przygotowane danych do analizy.¶
df['segment_pokemona'] = km.labels_
df.head()
df.segment_pokemona.value_counts()
df.legendarny.value_counts(normalize = True)
11. Analiza segmentów.¶
segment_0 = df[df.segment_pokemona == 0]
segment_1 = df[df.segment_pokemona == 1]
segment_2 = df[df.segment_pokemona == 2]
segment_3 = df[df.segment_pokemona == 3]
segment_4 = df[df.segment_pokemona == 4]
11.1. Analiza zmiennej "legendarny".¶
print('Segment 0.')
print((segment_0.legendarny.value_counts(normalize = True) * 100).round(3))
print('')
print('Segment 1.')
print((segment_1.legendarny.value_counts(normalize = True) * 100).round(3))
print('')
print('Segment 2.')
print((segment_2.legendarny.value_counts(normalize = True) * 100).round(3))
print('')
print('Segment 3.')
print((segment_3.legendarny.value_counts(normalize = True) * 100).round(3))
print('')
print('Segment 4.')
print((segment_4.legendarny.value_counts(normalize = True) * 100).round(3))
11.2. Liczności poszczególnych kategorii.¶
format_dict = {'seg_1':'${0:,.0f}', 'seg_1':'${0:,.0f}','seg_2':'${0:,.0f}','seg_3':'${0:,.0f}'}
df_count_1 = pd.DataFrame({ 'segment_0' : (segment_0.typ_1.value_counts(normalize = True) * 100).round(3),
'segment_1' : (segment_1.typ_1.value_counts(normalize = True) * 100).round(3),
'segment_2' : (segment_2.typ_1.value_counts(normalize = True) * 100).round(3),
'segment_3' : (segment_3.typ_1.value_counts(normalize = True) * 100).round(3),
'segment_4' : (segment_4.typ_1.value_counts(normalize = True) * 100).round(3)})
df_count_2 = pd.DataFrame({ 'segment_0' : (segment_0.typ_2.value_counts(normalize = True) * 100).round(3),
'segment_1' : (segment_1.typ_2.value_counts(normalize = True) * 100).round(3),
'segment_2' : (segment_2.typ_2.value_counts(normalize = True) * 100).round(3),
'segment_3' : (segment_3.typ_2.value_counts(normalize = True) * 100).round(3),
'segment_4' : (segment_4.typ_2.value_counts(normalize = True) * 100).round(3)})
df_count_1.fillna(0, inplace = True)
df_count_2.fillna(0, inplace = True)
(df_count_1.style
.format(format_dict)
.background_gradient(subset=['segment_0', 'segment_1', 'segment_2', 'segment_3', 'segment_4'], cmap='Reds'))
(df_count_2.style
.format(format_dict)
.background_gradient(subset=['segment_0', 'segment_1', 'segment_2', 'segment_3', 'segment_4'], cmap='Reds'))
11.3. Statystyki dla całego zbioru - punkt odniesienia dla segmentów.¶
df[['atak', 'obrona', 'poziom_zycia']].agg(['mean', 'min', 'max'])
df[['typ_1', 'typ_2', 'legendarny']].describe()
11.4. Statystyki dla poszczególnych grup.¶
grouped = df[['segment_pokemona', 'atak', 'obrona', 'typ_1', 'typ_2', 'legendarny']].groupby('segment_pokemona').agg({"atak": [np.mean], "obrona": [np.mean], 'typ_1' : pd.Series.mode, 'typ_2' : pd.Series.mode, 'legendarny' : pd.Series.mode})
grouped.columns = ["_".join(x) for x in grouped.columns.ravel()]
grouped
df[['segment_pokemona', 'typ_1', 'typ_2', 'legendarny']].groupby('segment_pokemona').agg(pd.Series.mode)
12. Opis uzyskanych segmentów.¶
- Segment 0 - segment najliczniej reprezentowany przez pokemony normalne o niezłym ataku i przeciętnej offensywie. W tym segmencie odsetek pokemonów legendarnych jest bliski zeru. Nazwa segmentu: "normalne" pokemony.
- Segment 1 - tutaj dominują pokemony kamienne o przeciętnym ataku i świetnej obrona. Nazwa segmentu: kamienni twardziele.
- Segment 2 - przeciętny pokemon z tej grupy to pokemon wodny, który świetnie atakuje i równie dobrze się broni. Być może wynika to z tego, że segment ten posiada największy odsetek pokemonów legendarnych. Nazwa segmentu: legendarne pokemony wodne.
- Segment 3 - segment najliczniej reprezentowany przez pokemony normalne o słabym ataku i słabej offensywie. W tym segmencie odsetek pokemonów legendarnych jest zerowy. Nazwa segmentu: "normalne" pokemony.
- Segment 4 - podobnie jak w segmencie 3, tak i tutaj dominują pokemony wodne. To, co odróżnia ten segment od segmentu 3, to kiepski atak i kiepska obrona. Co ciekawe, to w ogóle nie znajdziemy tu pokemonów legendarnych, co może tłumaczyć tak słabe statystyki obrony i ataku. Nazwa segmentu: wodne przeciętniaki (albo kiepściaki ;)).
13. Podsumowanie.¶
Dziękuję Ci za dobrnięcie do końca. Mam nadzieję, że ten nieco luźniejszy projekt przypadł Ci do gustu. 😉 Jeśli masz jakiekolwiek uwagi, pytania, lub spostrzeżenia, to zapraszam do dyskusji w komentarzach pod wpisem. 🙂
PODOBAŁ CI SIĘ TEN ARTYKUŁ?
Jeśli tak, to zarejestruj się, by otrzymywać informacje o nowych wpisach.
Dodatkowo w prezencie wyślę Ci bezpłatny poradnik :-)
Dodaj komentarz