阴谋还是悲剧?- 基于机器学习假设检验视角,看泰坦尼克号事件

1. 引言

0x1:故事背景

泰坦尼克号(RMS Titanic),又译作铁达尼号,是英国白星航运公司下辖的一艘奥林匹克级邮轮,排水量46000吨,于1909年3月31日在北爱尔兰贝尔法斯特港的哈兰德与沃尔夫造船厂动工建造,1911年5月31日下水,1912年4月2日完工试航。
泰坦尼克号是当时世界上体积最庞大、内部设施最豪华的客运轮船,有“永不沉没”的美誉 。然而不幸的是,在它的处女航中,泰坦尼克号便遭厄运——它从英国南安普敦出发,途经法国瑟堡-奥克特维尔以及爱尔兰科夫(Cobh),驶向美国纽约。1912年4月14日23时40分左右,泰坦尼克号与一座冰山相撞,造成右舷船艏至船中部破裂,五间水密舱进水。次日凌晨2时20分左右,泰坦尼克船体断裂成两截后沉入大西洋底3700米处。2224名船员及乘客中,逾1500人丧生,其中仅333具罹难者遗体被寻回。泰坦尼克号沉没事故为和平时期死伤人数最为惨重的一次海难,其残骸直至1985年才被再度发现,目前受到联合国教育、科学及文化组织的保护。

这场耸人听闻的悲剧震惊了国际社会,并为船舶制定了更好的安全规定。造成海难失事的原因之一是乘客和机组人员没有足够的救生艇。尽管幸存下沉有一些运气因素,但有些人比其他人更容易生存,例如妇女,儿童和上流社会。

0x2:阴谋还是悲剧

在事件发生后,当时有很多阴谋论者怀疑泰坦尼克号事件是一个世纪大阴谋,是航运公司的一次骗保行为,一度争论了很久。

当然,笔者并不是历史学家,所以我们这次不从证据学的角度来分析这次事件。相反,我们尝试运用统计学的思维,从数据挖掘与分析层面来看一下,这次事故是真实的可能性有多少。

基本假设是这样的,我们首先建立一个二元对立假设,这是本文的最大的基础:

  • 如果是一次骗保行为,则航运公司的目标是将

    船给摧毁,除此之外不会有任何其他的行为,那么船长也不应该表现出任何的绅士和道德楷模风度,或者说,即使是作秀,表现出的道德程度也不应该很强。

  • 如果是一个真实事故,则所有人(包括船长)表现出的行为都是按照人的第一反应作出的,则按照事后幸存者的采访记录,副船长爱德华·约翰·史密斯表现出了惊人的绅士风度和道德楷模风范,他说出了那句著名的话,『 lady and kid first!』,而自己则坚守到最后,非常令人感动。

接下来,我们将通过对数据进行建模,从而看某些特征指标(妇女、小孩)和生还之间是否存在强相关关系,以此推断出上述二元对立的结果。

Relevant Link:

https://baike.baidu.com/item/%E6%B3%B0%E5%9D%A6%E5%B0%BC%E5%85%8B%E5%8F%B7/5677

2. 数据建模

0x1:数据集

原始数据集可以从kaggle这里下载得到,kaggle的原题是做predict的,所以将数据集分为了train/test set,但是笔者这里并不做预测任务,因此将其合并起来作为一个统一的有标数据集,百度网盘链接在这里。

链接: https://pan.baidu.com/s/18Ushn_Oms8Q0RLw6TGFMnQ 提取码: u5gt  

0x2:数据集字段探查

各个特征字段意义如下:

0x3:数据缺失值补充

在很多统计项目中,由于数据采集手段上的限制,缺失值是非常常见的。因此,这里我们先进行缺失值处理。

# -*- coding: utf-8 -*-

import pandas as pd
import numpy as np

if __name__ == ‘__main__‘:
    dataset_file = "full_dataset.csv"
    dataset = pd.read_csv(dataset_file)

    print(dataset.info())

可以看到,【Sex,Age,Ticket,Fare,Cabin,Embarked】这几个字段都存在缺失值。

缺失值处理的思路有以下两种:

  • 1. 默认填充值的范围[(mean - std) ,(mean + std)]。
  • 2. 将缺失的Age当做label,将其他列的属性当做特征,通过已有的Age的记录训练模型,来预测缺失的Age值。

这里采取方法1,以Age为例:

for dataset in full_data:
    age_avg = dataset[‘Age‘].mean()
    age_std = dataset[‘Age‘].std()

    age_null_count = dataset[‘Age‘].isnull().sum()
    age_default_list = np.random.randint(low=age_avg-age_std,high=age_avg+age_std,size=age_null_count)

    dataset[‘Age‘][np.isnan(dataset[‘Age‘])] = age_default_list
    dataset[‘Age‘] = dataset[‘Age‘].astype(int)

其他字段以此类推,这里不再赘述,文末会给出完整代码。

0x4:离散特征数值化处理

对于有限离散的字符串特征,还需要进行映射处理或者one-hot处理。

def Passenger_Embarked(x):
    Embarked = {‘S‘:0, ‘C‘:1, ‘Q‘:2}
    return Embarked[x]

0x5:特征二次处理 - 专家经验特征

除了原始数据集的基础特征之外,还可以基于专家经验进行组合特征,当然这可以通过AutoEncoder进行自动完成,之所以不这么做的原因是因为AE的内部机制不容易对外暴露,可解释性工作会变得很复杂,因为我们这里使用人工特征组合,并使用XGB进行模型拟合。

1. 家族名特征

在所有特征中注意到有一个Name字段,这是一个字符串特征,字符串本身是无法输入模型的。显然,我们不能直接将Name进行ascii向量化,因为名字本身是没有任何意义的,我们需要将其groupby处理后得到某种统计意义上的特征。

我们发现Name的title name是存在类别的关系的。于是可以对Name进行提取出称呼这一类别title name。

import re
def get_title_name(name):
    title_s = re.search(‘ ([A-Za-z]+)\.‘, name)
    if title_s:
        return title_s.group(1)
    return ""
for dataset in full_data:
    dataset[‘TitleName‘] = dataset[‘Name‘].apply(get_title_name)
print(pd.crosstab(train[‘TitleName‘],train[‘Sex‘]))
###### out
Sex        female  male
TitleName
Capt            0     1
Col             0     2
Countess        1     0
Don             0     1
Dr              1     6
Jonkheer        0     1
Lady            1     0
Major           0     2
Master          0    40
Miss          182     0
Mlle            2     0
Mme             1     0
Mr              0   517
Mrs           125     0
Ms              1     0
Rev             0     6
Sir             0     1
####可以看出不同的titlename中男女还是有区别的。进一步探索titlename对Survived的影响。
####看出上面的离散取值范围还是比较多,所以可以将较少的几类归为一个类别。
train[‘TitleName‘] = train[‘TitleName‘].replace(‘Mme‘, ‘Mrs‘)
train[‘TitleName‘] = train[‘TitleName‘].replace(‘Mlle‘, ‘Miss‘)
train[‘TitleName‘] = train[‘TitleName‘].replace(‘Ms‘, ‘Miss‘)
train[‘TitleName‘] = train[‘TitleName‘].replace([‘Lady‘, ‘Countess‘,‘Capt‘, ‘Col‘,     ‘Don‘, ‘Dr‘, ‘Major‘, ‘Rev‘, ‘Sir‘, ‘Jonkheer‘, ‘Dona‘], ‘Other‘)
print (train[[‘TitleName‘, ‘Survived‘]].groupby(‘TitleName‘, as_index=False).mean())
###### out1
  TitleName  Survived
0    Master  0.575000
1      Miss  0.702703
2        Mr  0.156673
3       Mrs  0.793651
4     Other  0.347826

可以看出TitleName对存活率还是有影响差异的,这就说明了TitleName中蕴含了信息,有信息就可以为模型所用。

从信息增益的角度出发,最后将TitleName总共为了5个类别:Mrs,Miss,Master,Mr,Other。

2. 家庭大小

SibSp和Parch分别为同船的兄弟姐妹和父母子女数,离散数据,没有缺失值。于是可以根据该人的家庭情况组合出不同的特征。

并且,根据FamilySize大小增加新特征IsAlone。

0x6:模型训练

# -*- coding: utf-8 -*-

import pandas as pd
import numpy as np
import re
from sklearn.preprocessing import LabelBinarizer
from sklearn.ensemble import RandomForestRegressor

import xgboost as xgb
from xgboost import plot_importance
import matplotlib.pyplot as plt

def Passenger_sex(x):
    sex = {‘female‘: 0, ‘male‘: 1}
    return sex[x]

def Passenger_Embarked(x):
    Embarked = {‘S‘: 0, ‘C‘: 1, ‘Q‘: 2}
    return Embarked[x]

def get_title_name(name):
    title_s = re.search(‘ ([A-Za-z]+)\.‘, name)
    if title_s:
        return title_s.group(1)
    return ""

def Passenger_TitleName(x):
    TitleName = {‘Mr‘:0,‘Miss‘:1, ‘Mrs‘:2, ‘Master‘:3, ‘Other‘:4}
    return TitleName[x]

def data_feature_engineering(full_data, age_default_avg=True, one_hot=True):
    """
    :param full_data:全部数据集,包括train,test
    :param age_default_avg: age默认填充方式,是否使用平均值进行填充
    :param one_hot: Embarked字符处理是否是one_hot编码还是映射处理
    :return:处理好的数据集
    """
    for dataset in full_data:
        # Pclass,Parch,SibSp不需要处理

        # Sex用 0,1做映射,{‘female‘:0, ‘male‘:1}
        dataset[‘Sex‘] = dataset[‘Sex‘].map(Passenger_sex).astype(int)

        # SibSp和Parch均没有空值,组合特征FamilySize
        dataset[‘FamilySize‘] = dataset[‘SibSp‘] + dataset[‘Parch‘] + 1

        # 根据FamilySize大小增加新特征IsAlone
        dataset[‘IsAlone‘] = 0
        isAlone_mask = dataset[‘FamilySize‘] == 1
        dataset.loc[isAlone_mask, ‘IsAlone‘] = 1

        # Fare数据存在空值,先填充
        fare_median = dataset[‘Fare‘].median()
        dataset[‘Fare‘] = dataset[‘Fare‘].fillna(fare_median)

        # Embarked存在空值,众数填充,映射处理或者one-hot 编码
        dataset[‘Embarked‘] = dataset[‘Embarked‘].fillna(‘S‘)
        dataset[‘Embarked‘] = dataset[‘Embarked‘].astype(str)
        if one_hot:
            # 因为OneHotEncoder只能编码数值型,所以此处使用LabelBinarizer进行独热编码
            Embarked_arr = LabelBinarizer().fit_transform(dataset[‘Embarked‘])
            dataset[‘Embarked_0‘] = Embarked_arr[:, 0]
            dataset[‘Embarked_1‘] = Embarked_arr[:, 1]
            dataset[‘Embarked_2‘] = Embarked_arr[:, 2]
            dataset.drop(‘Embarked‘, axis=1, inplace=True)
        else:
            # 字符映射处理
            dataset[‘Embarked‘] = dataset[‘Embarked‘].map(Passenger_Embarked).astype(int)

        # Name选取称呼Title_name,{‘Mr‘:0,‘Miss‘:1, ‘Mrs‘:2, ‘Master‘:3, ‘Other‘:4}
        dataset[‘TitleName‘] = dataset[‘Name‘].apply(get_title_name)
        dataset[‘TitleName‘] = dataset[‘TitleName‘].replace(‘Mme‘,‘Mrs‘)
        dataset[‘TitleName‘] = dataset[‘TitleName‘].replace(‘Mlle‘, ‘Miss‘)
        dataset[‘TitleName‘] = dataset[‘TitleName‘].replace(‘Ms‘, ‘Miss‘)
        dataset[‘TitleName‘] = dataset[‘TitleName‘].replace([‘Lady‘, ‘Countess‘, ‘Capt‘, ‘Col‘,                                                              ‘Don‘, ‘Dr‘, ‘Major‘, ‘Rev‘, ‘Sir‘, ‘Jonkheer‘, ‘Dona‘],
                                                            ‘Other‘)
        dataset[‘TitleName‘] = dataset[‘TitleName‘].map(Passenger_TitleName).astype(int)

        # age 缺失值,可以使用随机值填充,也可以用RF预测
        if age_default_avg:
            # 缺失值使用avg处理
            age_avg = dataset[‘Age‘].mean()
            age_std = dataset[‘Age‘].std()
            age_null_count = dataset[‘Age‘].isnull().sum()
            age_default_list = np.random.randint(low=age_avg-age_std, high=age_avg+age_std, size=age_null_count)
            dataset.loc[np.isnan(dataset[‘Age‘]),‘Age‘] = age_default_list
            dataset[‘Age‘] = dataset[‘Age‘].astype(int)

        else:
            # 将age作为label,使用RF的age
            # 特征为TitleName,Sex,pclass,SibSP,Parch,IsAlone,CategoricalFare,FamileSize,Embarked
            feature_list = [‘TitleName‘, ‘Sex‘, ‘Pclass‘, ‘SibSp‘, ‘Parch‘, ‘IsAlone‘, ‘CategoricalFare‘,
                            ‘FamilySize‘, ‘Embarked‘, ‘Age‘]
            if one_hot:
                feature_list.append(‘Embarked_0‘)
                feature_list.append(‘Embarked_1‘)
                feature_list.append(‘Embarked_2‘)
                feature_list.remove(‘Embarked‘)

            Age_data = dataset.loc[:, feature_list]
            un_Age_mask = np.isnan(Age_data[‘Age‘])
            Age_train = Age_data[~un_Age_mask]  #要训练的Age

            feature_list.remove(‘Age‘)

            rf0 = RandomForestRegressor(n_estimators=60, oob_score=True, min_samples_split=10, min_samples_leaf=2, max_depth=7, random_state=10)
            rf0.fit(Age_train[feature_list], Age_train[‘Age‘])

            def set_default_age(age):
                if np.isnan(age[‘Age‘]):
                    data_x = np.array(age.loc[feature_list]).reshape(1,-1)
                    age_v = np.round(rf0.predict(data_x))[0]
                    print(age_v)
                    return age_v
                return age[‘Age‘]

            dataset[‘Age‘] = dataset.apply(set_default_age, axis=1)

    return full_data

# 特征选择
def data_feature_select(full_data):
    """
    :param full_data:全部数据集
    :return:
    """
    drop_list = [‘PassengerId‘, ‘Name‘, ‘Ticket‘, ‘Cabin‘]
    for data_set in full_data:
        # remove meaningless feature
        data_set.drop(drop_list, axis=1, inplace=True)

    train_y = np.array(full_data[0][‘Survived‘])
    train = full_data[0].drop(‘Survived‘, axis=1, inplace=False)

    return train, train_y

def modelfit(alg,dtrain_x,dtrain_y,useTrainCV=True,cv_flods=5,early_stopping_rounds=50):
    """
    :param alg: 初始模型
    :param dtrain_x:训练数据X
    :param dtrain_y:训练数据y(label)
    :param useTrainCV: 是否使用cv函数来确定最佳n_estimators
    :param cv_flods:交叉验证的cv数
    :param early_stopping_rounds:在该数迭代次数之前,eval_metric都没有提升的话则停止
    """
    if useTrainCV:
        xgb_param = alg.get_xgb_params()
        xgtrain = xgb.DMatrix(dtrain_x,dtrain_y)
        print(alg.get_params()[‘n_estimators‘])
        cv_result = xgb.cv(xgb_param,xgtrain,num_boost_round = alg.get_params()[‘n_estimators‘],
                           nfold = cv_flods, metrics = ‘auc‘, early_stopping_rounds=early_stopping_rounds)

        print(cv_result)
        print(cv_result.shape[0])
        alg.set_params(n_estimators=cv_result.shape[0])

    # train data
    alg.fit(train_X,train_y,eval_metric=‘auc‘)

    #predict train data
    train_y_pre = alg.predict(train_X)

    print ("\nModel Report")
    print ("Accuracy : %.4g" % metrics.accuracy_score(train_y, train_y_pre))

    feat_imp = pd.Series(alg.get_booster().get_fscore()).sort_values(ascending=False)
    feat_imp.plot(kind = ‘bar‘,title=‘Feature Importance‘)
    plt.ylabel(‘Feature Importance Score‘)
    plt.show()
    print(alg)

if __name__ == ‘__main__‘:
    dataset_file = "./full_dataset.csv"
    full_data = pd.read_csv(dataset_file)
    full_data = [full_data]

    # feature engineering
    full_data = data_feature_engineering(full_data, age_default_avg=True, one_hot=False)

    # select feature
    train_X, train_y = data_feature_select(full_data)

    print train_X
    print train_y

    # train the model
    # 1. set booster param
    param = {‘max_depth‘: 7, ‘eta‘: 1, ‘silent‘: 1, ‘objective‘: ‘reg:linear‘}
    param[‘nthread‘] = 4
    param[‘seed‘] = 100
    param[‘eval_metric‘] = ‘auc‘
    num_round = 10
    dtrain = xgb.DMatrix(data=train_X, label=train_y, missing=-999.0)
    bst_xgboost_model = xgb.train(param, dtrain, num_round)

    # print the feature importance table
    ### plot feature importance
    fig, ax = plt.subplots(figsize=(15, 15))
    plot_importance(bst_xgboost_model, height = 0.5, ax=ax, max_num_features=64)
    plt.show()

3. 结果分析

Xgboost训练后得到的特征重要性评估表如下:

从结果中可以得到几点信息:

  • Age年龄确实是和是否生还强相关的,占据了主导地位
  • 紧随年龄之后,排名第二的生还因素是Fare(船票价格),这从一定程度上说明,不管是出于富人逃生意识更强,富人有更多机会上到顶层夹板玩了透气,还是说逃生的时候有意识地偏向了富人,财富在其中的作用一定程度上是存在的
  • 排名第三的是Sex(性别),这也副船长的lady fisrt的说法也是一致的
  • 往后的几个特征,从各个维度都说明了生还几率和财富有关

从数据和统计层面可以解读出很多信息,更深层次的猜测,读者朋友可以参阅下面一些链接:

https://baijiahao.baidu.com/s?id=1623250627137421869&wfr=spider&for=pc
https://baijiahao.baidu.com/s?id=1621654492865556041&wfr=spider&for=pc
https://baijiahao.baidu.com/s?id=1597619726579770526&wfr=spider&for=pc
http://www.ufo-1.cn/article/201604/896.html

同样的分析方法,也可以用于二战珍珠港事件以及911事件的分析,读者朋友可以Google之。

笔者这里想说的是,无论什么时代,什么时候,总会有喷子无脑地抛出阴谋论的说法,好像所有事情背后总有一个看不见的手在操纵之类的,但是,历史总是在上演各种各样的大事件,所有事件的发生与否从单个事件来看也许是偶然离奇事件,但放到具体历史背景下,合理性是不容置疑的。

相反,当某个巨大灾难发生时,当民族遇到危机,当国家遭到危难时,站出来的总是看起来沉默寡言的英雄,正是这些英雄的存在,慰藉了我们的心灵,给了我们温暖与前进的力量,向英雄致敬。

原文地址:https://www.cnblogs.com/LittleHann/p/11333930.html

时间: 2024-10-01 06:06:57

阴谋还是悲剧?- 基于机器学习假设检验视角,看泰坦尼克号事件的相关文章

基于机器学习的web异常检测

基于机器学习的web异常检测 Web防火墙是信息安全的第一道防线.随着网络技术的快速更新,新的黑客技术也层出不穷,为传统规则防火墙带来了挑战.传统web入侵检测技术通过维护规则集对入侵访问进行拦截.一方面,硬规则在灵活的黑客面前,很容易被绕过,且基于以往知识的规则集难以应对0day攻击:另一方面,攻防对抗水涨船高,防守方规则的构造和维护门槛高.成本大. 基于机器学习技术的新一代web入侵检测技术有望弥补传统规则集方法的不足,为web对抗的防守端带来新的发展和突破.机器学习方法能够基于大量数据进行

4星|《剑桥中国经济史:古代到19世纪》:经济学视角看中国古代史

剑桥中国经济史:古代到19世纪 万志英教授对自青铜时代至20世纪初这3000年间中国经济发展各种制度的基础.延续以及中断进行了详尽而通俗的研究 作者是海外汉学家.全书从经济学的角度重新梳理了中国古代史,有不少独到的视角与细节.比如说对古代史的分期,与我见过的任何一本书的分期都不同,唐朝以安史之乱为节点被拦腰分成两节归入前后两个时期,前半部分划入从北魏成熟期开始的中国再统一时期,后半部分到北宋灭亡属于第一次经济转型期. 书中涉及到大量历史书上不太提及的经济学问题,比如人民收入水平,粮食价格,实际的

(5)【整合】基于机器学习的酒店评论文本倾向性分析

酒店评论情感分析系统(五)—— [整合]基于机器学习的酒店评论文本倾向性分析 本文主要是对之前的第三.四节所介绍的各分块进行整合. 流程: 对GUI界面输入的评论文本,先进行中文分词和去停用词,然后再按“环境,价格,卫生,服务”四个方面分类:同时通过调用LingPipe中的DynamicLMClassifier,对已标注的“中文情感挖掘酒店评论语料”库的学习,构造一个基本极性分析的分类器.最后用此分类器分别对整个评论文本给出总体评价和对四个方面分别给出相应的情感评价. 环境: 1. 操作系统:w

【NLP】基于机器学习角度谈谈CRF

基于机器学习角度谈谈CRF 作者:白宁超 2016年8月3日08:39:14 [摘要]:条件随机场用于序列标注,数据分割等自然语言处理中,表现出很好的效果.在中文分词.中文人名识别和歧义消解等任务中都有应用.本文源于笔者做语句识别序列标注过程中,对条件随机场的了解,逐步研究基于自然语言处理方面的应用.成文主要源于自然语言处理.机器学习.统计学习方法和部分网上资料对CRF介绍的相关的相关,最后进行大量研究整理汇总成体系知识.文章布局如下:第一节介绍CRF相关的基础统计知识:第二节介绍基于自然语言角

3D 视角看 Go 并发编程

主题:3D 视角看 Go 并发编程 Overview 并发编程综述 Goroutine Channel & Select & waitGroup 消息传递模式 可视化工具 GoTrace 主讲师:PP 先后在百度.第四范式.蚂蚁金服工作,百度 Go Good Coder, 对分布式计算.离线/实时大数据处理有丰富的实战经验.乐于分享自己的技术和学习心得. 一.并发编程综述 串行执行 并发与并行 多核时代的并发编程 左图:p1, p2, p3 这 3 个线程运行在单核上,分时复用 CPU,是

[蓝牙] 2、蓝牙BLE协议及架构浅析&&基于广播超时待机说广播事件

第一章 BLE基本概念了解 一.蓝牙4.0和BLE区别 蓝牙4.0是一种应用非常广泛.基于2.4G射频的低功耗无线通讯技术.蓝牙低功耗(Bluetooth Low Energy ),人们又常称之为BlueTooth Smart,是由SIG( the Bluetooth Special Interest Group) 在2010年6月起草,在原有标准的蓝牙4.0核心协议上添加的一种低功耗技术. 蓝牙低功耗不等同于蓝牙4.0,只是蓝牙4.0的一个分支.蓝牙4.0是蓝牙3.0+ HS(高速蓝牙)规范的

LibRec: 基于机器学习的大数据推荐系统

简介: LibRec 是领先的推荐系统Java开源算法工具库,覆盖了70余个各类型推荐算法,有效解决评分预测和物品推荐两大关键的推荐问题.推荐系统是机器学习和大数据技术的经典实际应用,旨在提供高效准确的个性化物品推荐,是现代Web应用的重要组件. 项目结构清晰,代码风格良好,测试充分,注释与手册完善.目前项目使用GPL3.0协议开源在github中,欢迎大家试用. Librec: http://www.librec.net/ GithubRepo:  https://github.com/guo

Linux ELF格式文件解析之relocation和entry入口点,基于gas汇编语言视角

问题引入: Linux的elf文件一开始理解起来的确很难,有的人可能会去看<linkers and loaders>,这书的确好,但是没有详细的解释很多细节,尤其是从汇编语言视角.我读了这本书很多地方后还是不明白,然后我又读了IBM360计算机的汇编器设计文档,里面详解了二次扫描汇编器的设计原理,以及relocation概念.但这些依然解决不了我的疑惑,因为困扰我的是一个选项,即ld -Ttext=org,我不明白这个org偏移会对程序产生什么影响,一开始我以为这选项只不过更改了elf的文件头

从数学的视角看社交网络

社交网络是有趣而又令人迷惑的,总希望希望从社交网络本身找到一些对移动互联网产品有启发的东西,而自己的社交网络有限,只得求助于专家们的著作了,这点文字是<社交网络分析-方法与实践>一书(地铁阅读时光)的笔记. 社交网络分析(SNA)是探索关系背后的科学与技术,从数学的角度看社交网络,用图论的方法探查社交网络.在技术上,通过python 以及相关包文件的支持,来完成可计算的社交网络(http://www.github.com/maksim2042/SNABook). 什么是关系呢?在人际关系中,除