精品教学案例 | 权利的游戏:战争数据分析

2020-05-13 10:31:26 浏览数 (1)

本案例适合作为大数据专业Pandas数据分析课程的配套教学案例。通过本案例,能够达到以下教学效果:

  • 培养学生对真实数据进行初步探索的能力。案例基于电视剧《权力的游戏》中关于战争的数据集,探索故事中三个主要阶段的数据。
  • 帮助学生熟悉常见的数据切片操作方法。例如:“[ ]”方法、“.loc”方法和“.iloc”方法。
  • 提高学生动手实践能力。案例中使用Pandas和Matplotlib工具对数据进行切片和可视化操作,提高学生对工具的使用熟练程度。

1 认识数据

在数据分析和数据挖掘等数据工作过程中,数据切片是最基础也是非常重要的一部分。在实际工作过程中,数据规模往往较大,根据不同的要求,往往需要选取某种形式的数据子集进行观察或处理。Pandas提供了多种不同的方法进行数据索引切片,比如[ ], .loc, 和.iloc等方法。本案例将会具体展示如何运用这些方法对数据集进行索引切片,并获得所需要的数据。

《权力的游戏》(Game of Thrones),是美国HBO电视网制作推出的一部中世纪史诗奇幻题材的电视剧。该剧改编自美国作家乔治·R·R·马丁的奇幻小说《冰与火之歌》系列。该剧成功塑造了成千上万形象饱满的人物角色、怪诞独特充满想象的风土人情,其空间之完整、细节之丰富、叙事之恣意让人感叹!本案例使用的数据集收集了《权力的游戏》小说中关于五王之战的信息,那么现在让我们用数据分析的方式看一看这个残酷的世界!

首先,我们导入Pandas库和NumPy库,读入数据,并查看数据集:

代码语言:javascript复制
import numpy as np
import pandas as pd
df = pd.read_csv('./input/game-of-thrones_battles.csv')
df.head()

通过简单的观察,可以看出数据集中有很多的缺失信息,为了方便处理,本案例选择数据集中的一些重要的特征来进行简单的分析,下列表是对这些特征的简单介绍:

下面用这些特征生成新的数据集,并将battle_number设置为数据集的索引:

代码语言:javascript复制
battles = df[['name', 'year', 'battle_number', 'attacker_king', 'defender_king','attacker_outcome','battle_type',
       'attacker_1', 'defender_1',  'attacker_size',
       'defender_size', 'attacker_commander', 'defender_commander', 'summer','location', 'region']].copy()
battles.set_index('battle_number', inplace=True)
battles.head()

下面导入用于数据可视化的包:

代码语言:javascript复制
import matplotlib.pyplot as plt
%matplotlib inline

简单观察一下每年发生了多少战争:

代码语言:javascript复制
battles['year'].value_counts().sort_index().plot.bar(figsize=(8, 6))
plt.xlabel("year")
plt.ylabel("battle_number")
plt.xticks(rotation=0)

从图中可以看出,随着年份的增长,发动战争的数量先增长后下降,恰好对应着五王之战的开端,高潮与落幕。本案例的组织结构便是分别探索战争的这三个阶段,用数据分析每一个阶段发生的故事。

事实上,上面的代码就是用了方括号法[ ]来选取我们需要的数据子集,后续会继续展开讨论这种切片方法。为了更加形象的展示数据,本案例也会像上面这样对部分数据切片进行简单的可视化,但不会详细地解释,有兴趣的读者可以查看数据可视化的其它案例。

下面让我们开始一边欣赏数据给我们叙述的故事,一边学习利用Pandas对数据进行切片的方法吧!

2 “狼狮”之争

2.1 故事简介

在A.C298年,五王之战起源于七国的两大家族——以狮子为家族徽章的兰尼斯特(Lannister)家族和以冰原狼为家族徽章的史塔克(Stark)家族之间的权力的纠纷。当时七国的首相艾德·史塔克发现王位的两位继承人乔佛里/托曼·拜拉席恩(Joffrey/Tommen Baratheon)其实都不是国王的孩子,而是王后瑟曦·兰尼斯特与其弟詹姆·兰尼斯特所生。兰尼斯特家族为了巩固自己的权力,设计迫害艾德·史塔克。为了救出自己的父亲,罗柏·史塔克(Robb Stark)率军南下,与兰尼斯特家族展开了权力的争夺。下面让我们看看数据是如何叙述这一时期的故事吧!

2.2 选取战争初始阶段的信息

首先我们选取在A.C298年间的战争的数据:

代码语言:javascript复制
battles_298 = battles[battles['year'] == 298]
battles_298

从这数据集中,我们可以找到很多有趣的信息,你可能会对这些问题感兴趣:

  1. 战争的开始是由谁引发的?
  2. 战斗双方的兵力对比怎么样?
  3. 这一年爆发了几场大规模战争?

在上一行代码中,我们简单的用了数据切片操作中的[ ]方法以及布尔索引,来得到“狼狮”之争这一时期的数据集,不过为了更好的回答上述问题,我们需要数据集展示的数据只与我们的问题相关,为了获得这样的数据集,我们需要进一步进行切片操作,下面本案例会对切片操作中的[ ]方法进行简单介绍,并用这些方法将数据集切片成我们需要的形式。

2.3 切片操作之[ ]方法

[ ]方法是很常见的数据切片方法,用起来简单方便,只不过有些局限性,主要是用于选取列数据。

使用[ ]方法,我们需要注意参数和得到的结果之间的关系:

  1. 如果参数为String类型,则返回列数据,为Series类型
  2. 如果参数为String类型的List,则返回列数据,为DataFrame类型
  3. 如果参数为布尔类型,则返回所有对应值为True的行数据
  4. 如果参数为切片(Slice)类型,则返回行数据

注意[ ]主要用于选择列数据,但是应用布尔索引时,可以选取行数据;当使用切片类型时,也可以返回行数据,既可以按位置选取也可以按标签选取,较容易混淆。

下面我们对这些不同的参数类型进行举例讲解。

如果我们想知道在A.C298年间发生了哪些战争,我们可以用[ ]方法来选取name列:

代码语言:javascript复制
battles_298['name']

此时参数是String类型,得到一个Series类型数据,记录了A.C298年间发生的战争名称。

如果只是选取一列数据,还有一种方法也能得到上述相同的效果——点方法:

代码语言:javascript复制
battles_298.name

至于用哪种方式可看个人的习惯,不过需要注意的是使用点方法时,若特征的名称里有空格字符就会失效,这时只能用[ ]方法。

还有一点不同的是,点方法不能选取多列,当想要选取多列时,也只能用[ ]方法, 下面选取A.C298年间发生冲突的双方国王:

代码语言:javascript复制
battles_298[['attacker_king', 'defender_king']]

此时参数为String组成的列表,选取两列数据,得到得到了DataFrame型数据,从数据上可以看出这一时期主要是兰尼斯特家族和史塔克家族之间的冲突。

**注意:**选取多列或者想要获得DataFrame类型数据的[ ]方法的语法为data[['columns1', 'columns2',……]],参数是一个列表,请注意不要犯这样的错误:data['columns1', 'columns2',……]

下面我们还想知道这两大家族的军事力量如何,继续选取一些特征:

代码语言:javascript复制
battles_298[['attacker_king', 'defender_king', 'attacker_outcome', 'attacker_size', 'defender_size']]

从数据集中可以看出总体上兰尼斯特家族军事实力和史塔克家族相比是占上风的,兰尼斯特赢了五场,而史塔克只赢了两场,不过这两场都是以少胜多,为了获得这两场战斗的全部信息,我们也可以使用参数为切片的类型的[ ]方法获得行数据:

代码语言:javascript复制
battles_298[4:6]

注意我们这边的切片形式[n:m]中的数字指的是位置信息,是从0开始计数的,所以这边会选取从第n 1行到第m行的数据。 从这两场战斗来看,都是由罗柏·史塔克领导的伏击战(ambush),甚至俘获了詹姆·兰尼斯特,可以说是大获全胜。据说这位少狼主是一位打仗的天才,在他带领的战争中都没有输过,只可惜最后还是被设计陷害了。

注: 虽然[ ]方法可以这样选取行数据,不过更为常用的选取行数据的方法还是后续要讲的.loc方法或.iloc方法,或者用布尔索引的方式。

当参数为布尔类型时,这时我们也称这种索引方法为布尔索引,布尔索引可以理解为条件索引,利用条件和逻辑符号限制选取行和列生成数据子集,布尔索引六种常用的操作符号为:><>=<===!=。我们可以用这些操作符号表达我们想要选取的条件,比如一开始我们便是使用布尔索引去选取年份为A.C298年的战争的信息,这个条件可以表示为battles['year'] == 298,这时返回的是一个布尔型的Series数据类型:

代码语言:javascript复制
bool_year_298 = battles['year'] == 298
bool_year_298.head(10)

这时这个Series只有前7行是True,后面的全是False,将这个布尔型Series对象作为参数传输到[ ]方法中,则返回所有对应值为True的行数据, 得到满足年份为298这个条件的数据子集:

代码语言:javascript复制
battles[bool_year_298]

当然我们可以把这两步结合起来,简写为

代码语言:javascript复制
battles[battles['year'] == 298]

作为另一个布尔索引的例子,我们想看看这一时期有多少大规模战斗,如果我们定义大规模战斗是参战人数不少于15000人,那么定义大规模战斗的条件就是all_size >= 15000,我们在数据集上添加一个特征all_size,记录每场战斗的参战人数,然后用这个特征过滤数据集:

代码语言:javascript复制
all_size = battles_298['attacker_size']   battles_298['defender_size']
df_298 = battles_298.copy()
df_298['all_size'] = all_size
df_298[df_298['all_size'] >= 15000]

从上面的数据中我们便可以得到答案了,这一年中一共有四场大规模战斗。

上面的代码也可以看出,对数据进行切片后也可以用来进行运算去生成新的特征,这也是需要对数据进行切片操作的原因之一。至于代码中的.copy()方法是为了避免SettingWithCopy警告,在一行代码中多次出现切片操作后进行赋值可能会有SettingWithCopy警告, 有兴趣的读者可以试一试代码:

代码语言:javascript复制
battles_298['all_size']  = battles_298['attacker_size']   battles_298['defender_size']

虽然这样操作也可能会在数据集上生成all_size特征,不过也会产生SettingWithCopy警告,所以最好不要采用这种链式赋值的形式。 除了.copy()方法,采用后面要介绍的.loc方法也可以避免SettingWithCopy警告。

另外,为了更直观的展示数据,我们也可以将战斗规模的数据进行可视化:

代码语言:javascript复制
df_298.set_index('name').all_size.plot.barh(figsize=(8,6))
plt.xlabel('battles_size')

3 五王争霸

3.1 故事简介

随着劳勃·拜拉席恩国王和首相艾德·史塔克的去世,乔弗里·拜拉席恩在君临登上铁王座,由于无法或不愿意接受乔佛里的王位正统性,四个敌对的国王很快在维斯特洛展开权力争夺,这四个国王是罗柏·史塔克(Robb Stark),蓝礼·拜拉席恩(Renly Baratheon),史坦尼斯·拜拉席恩(Stannis Baratheon),巴隆/攸伦·葛雷乔伊(Balon/Euron Greyjoy)。这一年中虽然罗柏·史塔克常常取得胜利,但是葛雷乔伊家族却乘虚而入,攻陷了临冬城(Winterfell),这让罗柏·史塔克首尾难顾。与此同时,蓝礼·拜拉席恩被史坦尼斯·拜拉席恩的女巫用黑魔法杀害,因此史坦尼斯吞并了蓝礼的军队,不过却在黑水河之战(Battle of the Blackwater)中被泰温·兰尼斯特(Tywin Lannister)打败。之后泰温·兰尼斯特与瓦德·弗雷(Walder Frey)合谋在血色的婚礼(The Red Wedding)中将罗柏·史塔克杀害,至此蓝礼·拜拉席恩和罗柏·史塔克的死,以及史坦尼斯·拜拉席恩在黑水湾的战败,几乎所有维斯特洛人都臣服于铁王座。

3.2 选取战争中间阶段的信息

下面我们换一种切片操作方法.loc方法来选取下一年——即在A.C299年间的战争的数据:

代码语言:javascript复制
battles_299 = battles.loc[battles['year'] == 299]
battles_299.head()

对于这一年的战争数据进行适当的切片操作处理,可以从中得到一些故事中的信息:

  1. 有哪些国王参与了权力的争斗?
  2. 故事里面提到的几大关键战役,有什么值得关注的信息吗?
  3. 北方战争的结果是什么,有什么影响?

下面我们主要采用.loc的切片操作方法来获得我们需要的确切数据形式。

3.3 切片操作之.loc方法

.loc方法可以根据行列标签选取数据,即基于列label以及行index选取数据,在选取行数据方面,相比于[ ]方法,.loc方法更为常用。

下面我们将举例说明.loc方法的几种参数形式:

  1. 行列标签
  2. 行列标签列表或数组
  3. 行列标签切片
  4. 布尔值列表或数组

有时我们也将这些参数混合使用,下面将按照这几种参数形式举例讲解.loc方法。

在这里为了体现.loc方法基于标签进行索引的特性,我们将数据name设置为数据的索引:

代码语言:javascript复制
battles_299.set_index('name',inplace=True)
battles_299.head()

如果我们想简单的看看血色婚礼的攻击者都有哪些人:

代码语言:javascript复制
battles_299.loc['The Red Wedding','attacker_commander']

参数都是行列标签,由此可以知道在婚宴上罗柏是被弗雷一家和波顿一家给设计陷害了。

当参数为行列标签列表或行列标签切片时,.loc方法便可以得到Series或DataFrame数据类型。比如:若我们想知道A.C299年战争的参与者都有哪些国王,可以用这种参数选取数据集后再进行分析:

代码语言:javascript复制
battles_kings = battles_299.loc[:,['attacker_king','defender_king']]
battles_kings.head()

第一个参数是行标签切片类型,参数:表示选取所有的行;第二个参数是列标签列表类型,得到对应列的DataFrame类型数据。

代码语言:javascript复制
set(battles_kings.attacker_king.unique()) | set(battles_kings.defender_king.unique())

正如数据和故事所讲,在A.C299年,两个家族的权力纷争最终演化为五王的争霸之战。

若想知道这一年中哪些国王参与战争次数最多:

代码语言:javascript复制
attacker_num = battles_299.loc[:,'attacker_king'].value_counts()
defender_num = battles_299.loc[:,'defender_king'].value_counts()
attacker_num.add(defender_num, fill_value=0).plot.barh(figsize=(8, 6))#add()函数中的fill_value参数可以解决不重叠位置相加出现缺失值的现象
plt.xlabel("Battle_number")
plt.ylabel("Kings")

从图中可以看出兰尼斯特家族,史塔克家族,以及葛雷乔伊家族在这一年的战争中比较活跃。

对于第二个问题,我们想对比下故事里比较关键的几大战役的信息,也可以使用这种参数形式的.loc方法来选取多行多列:

代码语言:javascript复制
battle_299_vital = battles_299 .loc[["Siege of Storm's End", "Battle of the Blackwater", "The Red Wedding"],:]
battle_299_vital

第一个参数是行标签列表形式,第二个参数是列标签切片形式,得到了DataFrame类型数据,包含了“风暴城之战”,“黑水之战”和“血色婚礼”这三个比较关键的战役。为了更具体的分析数据,下面继续用.loc切片方法分别选取这三大战役的信息:

代码语言:javascript复制
battle_299_vital.loc["Siege of Storm's End", ['attacker_king','defender_king','attacker_outcome','attacker_size','defender_size','location']]

从上面信息可以看出,风暴城之战是拜拉席恩兄弟间的战争,由于史坦尼斯和蓝礼都宣称自己才是铁王座的继承人,所以他们发动了这场战役,从数据上也可以看出史坦尼斯的军队实力弱于蓝礼,可是史坦尼斯却派女巫用黑魔法的手段杀害了蓝礼,并吞并了他的军队。之后,他才有实力去攻打君临城,发动黑水河之战。

代码语言:javascript复制
battle_299_vital.loc['Battle of the Blackwater', ['attacker_king','defender_king','attacker_outcome','attacker_size','defender_size','location']]

从这里的信息可以看出在黑水河之战中,史坦尼斯·拜拉席恩军队的人数在2万左右,这也印证了他在风暴城之战中吞并了他兄弟的军队,因此他在军事力量上是强过兰尼斯特家族的,而且已经打到了七国的首都君临城(King's Landing),如果史坦尼斯获胜,那后面的发展便会是另一个故事了,不过可惜的是史坦尼斯在这么大优势下输掉了战争。经过这场战争,史坦尼斯失去了登上铁王座的实力,再也无法和兰尼斯特家族抗衡。

代码语言:javascript复制
battle_299_vital.loc['The Red Wedding', ['attacker_king','defender_king','attacker_outcome','battle_type','attacker_commander','defender_commander']]

在血色婚礼上,罗柏·史塔克被弗雷家族和波顿家族用伏击的方式杀害了,在失去了少狼主之后,北境各个领主四分五裂,也没有力量继续侵犯南方了。

对于最后问题的回答,若我们想获取北方战争的一些信息,也可以采用参数为布尔值列表或数组的.loc方法:

代码语言:javascript复制
battles_299.loc[battles_299.region == 'The North', 'attacker_king':'attacker_commander']

这是.loc方法中的布尔索引,第一个参数为布尔型Series对象,用于选取在北方发生的一些战争的行数据,第二个参数是列标签切片类型,选取用于展示的列,生成DataFrame数据类型。从数据中可以看出,这时主要是葛雷乔伊家族趁着临冬城空虚,在北方发动侵略战争,并取得了全面的胜利,最终席恩·葛雷乔伊(Theon Greyjoy)靠着20人的伏击战在临冬城之战中(Battle of Winterfell)占领了临冬城,这让罗柏·史塔克腹背受敌并影响了他之后的战略方针,也为他落入血色婚礼的陷阱埋下伏笔。

4 凛冬来临

4.1 故事简介

与现实世界不同,在权力的游戏的虚构世界里,季节是按年份不规则的交替轮换,A.C300是冬季的一年,而且学者们也预言这个冬季也会是有史以来最长的一个冬季,"Winter is coming"(“凛冬将至”)是临冬城史塔克家族的族语,缘于其家族领地临冬城以及对北镜之外未知事物时刻保持警惕敬畏之心。在战争的末期,虽然各个家族依然有纷争,可是其他家族再难有力量和兰尼斯特家族争夺权力。

4.2 选取战争最后阶段的信息

下面我们用.iloc方法选取在A.C298年间的战争的数据:

代码语言:javascript复制
battles_300 = battles.iloc[list(battles['year'] == 300)]
battles_300.head()

与之前的方法不同,.iloc方法接受布尔型列表或数组,但不接受布尔型Series对象,所以用上面这种代码形式索引A.C300年间的战争的数据。

类似的,我们计划这一节用.iloc方法进行切片处理,试图回答下面几个问题:

  1. 冬季是从A.C300年开始的吗?
  2. 战争末期还有什么大的战役吗?

下面我们将介绍.iloc方法进行切片操作。

4.3 切片操作之.iloc方法

.iloc方法是根据位置的整数索引选取数据,位置索引从0开始,虽然索引的参数不同,但能得到与.loc方法相同的效果。

.iloc[ ]方法提供了几种参数形式:

  1. 整数
  2. 整数列表或数组
  3. 整数切片
  4. 布尔型列表或数组

同样的,下面会根据这些参数举例介绍.iloc方法的用法。

.iloc方法是根据位置选取数据,如果数据集的索引形式是RangeIndex形式,那会更好的帮助我们确定位置,但也要注意到索引名是从哪开始的,比如,我想选取总数据集battles的第一列:

代码语言:javascript复制
battles.iloc[0]

由于参数是整数,所以得到Series数据类型,若想得到DataFrame数据类型,可以采用数据列表或数组的形式:

代码语言:javascript复制
battles.iloc[[0]]

得到DataFrame对象,这时可以看到第一列的索引是从1开始的,所以用整数的切片形式的时候也需要记住.iloc方法使根据位置进行索引,比如选取前两行两列:

代码语言:javascript复制
battles.iloc[0:2,0:2]

即选取位置为0,1的行列数据。

了解了.iloc方法与其他方法的区别后,我们开始用.iloc方法对数据集切片操作,获得我们需要的数据集。

下面,如果想要了解冬季是不是从A.C300年才开始的,我们可以用.iloc方法的布尔索引:

代码语言:javascript复制
battles.iloc[list(battles['summer'] == 0.0)].head(5)

得到在冬季发生战争的数据集,发现得到的数据集和之前得到的battles_300数据集一样,所以说在A.C300年,凛冬已经到来了。

至于第二个问题,也是和前面小节一样,添加新变量,获得参战人员总数, 并用布尔索引获得数据:

代码语言:javascript复制
all_size_300 = battles_300.attacker_size.add(battles_300.defender_size,fill_value=0)
df_300 = battles_300.copy()
df_300['all_size_300'] = all_size_300
df_300.iloc[list(df_300.all_size_300 >= 15000)]

.iloc方法索引战争规模的数据,并进行可视化:

代码语言:javascript复制
df_300.set_index('name').iloc[:,-1].plot.barh(figsize=(8, 6))
plt.xlabel('battles_size')

可以看出在这一时期只有一场大战,还是来自于北方长城之外的自由民(Free folk)为了逃避北方异鬼的威胁,想要到南方去避难而与守夜人进行的战争。而其他家族再也没有实力去发动一场大战了,因此五王之战慢慢落下帷幕,政治上也获得了短暂的平衡。

注意在这条数据上有一点错误,这场战争是塞外之王曼斯·雷德(Mance Rayder)发动的,所以attacker_kingdefender_king是错误的,史坦尼斯并没有那么强的军事力量,而且最后战争输的是曼斯·雷德。下面我们也可以用切片操作对数据进行修改,这种链式赋值很容易产生SettingWithCopy警告,为了避免这种警告除了前面提到过的可以重新创立一个副本,也可以用.loc.iloc方法进行:

代码语言:javascript复制
battles.iloc[battles.index == 28, 2] = 'Mance Rayder'
battles.loc[battles.index == 28, 'defender_king'] = 'Stannis Baratheon'
battles.iloc[[27]]

总之,五王之战给维斯特洛大陆造成了巨大的破坏和死亡。敌对军队焚烧庄稼和食物,意味着随着冬天从公元300年开始席卷维斯特洛大陆,更多的人将会饿死。再加上海外龙母对铁王座的势在必得,北方野人的入侵以及长城外异鬼的威胁,这段短暂的政治平衡又能维持到什么时候呢?维斯特洛大陆上的人民的命运又将会如何呢?由于数据集收集的是在这之前的数据,想要了解故事后续发展的读者可以阅读《冰与火之歌》书籍或者观看《权力的游戏》系列电视剧。

5. 总结

《权力的游戏》的故事讲到这里就结束了,下面我们简要回顾一下本案例介绍的要点以及读者需要掌握的知识点:

  1. 了解数据集的三种切片操作方式的特点。
  2. 学会设置这三种切片操作的参数,以获取想要的数据集。
  3. 熟练掌握布尔索引。
  4. 学会使用.loc.iloc方法,以避免链式赋值出现的警告。
  5. 练习通过切片操作以及可视化操作进行简单的数据分析。

0 人点赞