大数跨境
0
0

数据预处理|12 数据融合和数据整合

数据预处理|12 数据融合和数据整合 谁说菜鸟不会数据分析
2023-02-03
1
导读:对数据预处理的普遍理解是与数据清洗齐头并进的。 虽然数据清洗是数据预处理的一个主要和重要的部分,但关于这个主

对数据预处理的普遍理解是与数据清洗齐头并进的。 虽然数据清洗是数据预处理的一个主要和重要的部分,但关于这个主题还有其他重要的领域。在本章中,我们将了解其中两个重要领域:数据融合和数据整合。简而言之,数据融合和整合与混合两个或多个数据源以实现分析目标有很大关系。 首先,我们将了解到数据融合和数据整合的异同。之后,我们将了解有关数据融合和数据整合的六个常见挑战。然后,通过观察三个完整的分析例子,我们将遇到这些挑战并加以处理。 在本章中,我们将涵盖以下主要议题。

  • • 什么是数据融合和数据整合?

  • • 关于数据融合和整合的频繁挑战

  • • 例子1(挑战3和4)

  • • 例子2(挑战2和3)

  • • 例子3(挑战1、2、3、5和6)

1 什么是数据融合和数据整合?

在大多数情况下,数据融合和数据整合是可以互换使用的术语,但它们之间存在着概念和技术上的区别。我们将很快讨论这些。让我们从两者的共同点和它们的含义开始。当我们的分析目标所需要的数据来自不同的来源时,在我们进行数据分析之前,我们需要将这些数据源整合成我们的分析目标所需要的一个数据集。下图直观地总结了这种整合。

image.png

在现实世界中,数据整合要比前述图中所示的困难得多。在整合之前,有许多挑战需要克服。这些挑战可能是由于组织的隐私和安全挑战限制了我们的数据可及性。但是,即使假设这些挑战在不同的数据源需要整合时不碍事,它们的出现是因为每个数据源都是根据需求、标准、技术和收集者的意见来收集和构建的。不管正确与否,数据的结构化方式总是存在差异,正因为如此,数据整合变得很有挑战性。 在本章中,我们将介绍最经常面临的数据集成挑战,并学习如何处理这些挑战。这些挑战将在下一个分章中讨论。 首先,让我们了解一下数据融合和数据整合之间的区别。

1.1 数据融合与数据整合

正如我们之前所暗示的,数据整合(data integration)和数据融合(data fusion)都是关于混合一个以上的数据源。在数据集成方面,混合行为比较容易,因为所有的数据源都有相同的数据对象定义,或者通过简单的数据重组或转换,数据对象的定义可以变得相同。当数据对象的定义是相同的,并且数据对象在各数据源中的索引是相似的,混合数据源就变得很容易;这将是一行代码的事。 这就是数据整合的作用;它在数据源之间匹配数据对象的定义,然后混合数据对象。 另一方面,当数据源与数据对象的定义不一致时,就需要数据融合。通过重组和简单的数据转换,我们无法在不同的数据源中创建相同的数据对象定义。对于数据融合,我们通常需要想象一个对所有数据源来说都可能存在的数据对象定义,然后对数据进行假设。基于这些假设,我们必须对数据源进行重组。因此,同样,数据源处于具有相同的数据对象定义的状态。在这一点上,混合数据源的行为变得非常容易,只需一行代码就可以完成。 让我们通过两个例子来尝试和理解两者之间的区别:一个需要数据整合,一个需要数据融合。

数据整合实例

想象一下,一家公司想分析它在广告方面的有效性。该公司需要拿出两列数据--每个客户的总销售额和每个客户的广告支出总额。由于销售部门和市场部门保存和管理他们的数据库,每个部门将负责创建一个具有相关信息的客户名单。一旦他们完成了这一工作,他们需要将两个来源的每个客户的数据连接起来。这种连接可以依靠真实客户的存在来实现,所以不需要做任何假设。不需要做任何改变来连接这些数据。这是一个明显的数据整合的例子。两个来源的数据对象的定义都是客户。

数据融合实例

想象一下,一个有技术能力的农民想看看灌溉(水的分散性)对产量的影响。农民拥有关于其旋转水站所分配的水量和农场中每个点的收获量的数据。每个固定的水站都有一个传感器,并计算和记录所分配的水量。另外,联合收割机的刀片每次移动时,机器会计算并记录收割量和位置。 在这个例子中,数据来源之间没有明确的联系。在前面的例子中,明确的联系是数据对象的定义--客户。然而,我们在这里没有这个条件,所以我们需要做出假设并改变数据,这样才有可能建立起联系。这个例子中的情况可能像下图一样。蓝色的点代表水站,而灰色的代表收获点。

image.png

为了进行数据融合,我们需要不同的假设集和预处理集来结合或融合这些数据源。接下来,让我们看看这些使融合成为可能的假设是什么样子的。 如果我们把我们的数据对象定义为被收割的土地,那会怎么样?换句话说,我们把我们的数据对象定义为收获点。然后,根据旋转水站与每个收获点的距离,我们计算出一个数字,代表该点得到的水量。每个水点都可以被赋予一个到达半径。 一个收获点离这个到达半径内的水点越近,该收获点从水站分配的水量中得到的就越多。 我们不知道有多少水到达收获点,但我们对它进行了假设。这些假设可能是完全幼稚的,也可能是基于一些仔细的实验或研究。 在这个例子中,我们不得不想出一个在两个数据源中都不存在的数据对象的定义。然后,我们不得不对收集到的数据做出许多假设,以便数据源可以被融合。 好消息! 在本章末尾的练习8中,你将亲自进行这种数据融合。 在本章中,你会看到数据整合和数据融合这两个术语。当你需要注意它们之间的区别时,文中会告诉你。 你几乎可以开始看到在这些领域经常发生的挑战,以及一些例子,但首先,让我们再讨论一件事。在下一节,我们将介绍数据整合的两个方向。

1.2 数据整合的方向

数据整合可能发生在两个不同的方向。第一个是通过添加属性;我们可能想用更多的描述性属性来补充一个数据集。在这个方向上,我们有所有我们需要的数据对象,但其他来源可能会丰富我们的数据集。第二种是通过增加数据对象;我们可能有多个数据源,有不同的数据对象,将它们整合起来,就会有更多的数据对象来代表我们想要分析的人群。 让我们来看看两个例子,以便更好地理解数据整合的两个方向。

通过添加属性进行数据整合的例子

我们在前面的数据整合例子和数据融合例子部分看到的例子都是通过添加属性进行数据整合。在这些例子中,我们的目的是通过包括更多对分析目标有益或必要的属性来补充数据集。在这两个例子中,我们研究了需要通过添加属性进行数据整合的情况。接下来,我们将研究需要通过添加数据对象进行数据整合的情况。

通过添加数据对象进行数据整合的例子

在第一个例子中(数据整合例子),我们想整合来自销售和市场部门的客户数据。数据对象和客户是相同的,但不同的数据库包含了我们分析目标所需的数据。现在,想象一下,公司有五个区域管理机构,每个管理机构负责保存他们的客户数据。在这种情况下,数据整合将发生在每个管理机构拿出一个数据集,包括每个客户的总销售额和每个客户的广告支出总额之后。这种类型的整合,我们使用五个数据源,其中包括不同客户的数据,被称为通过添加数据对象进行数据整合。 在第二个例子中(数据融合例子),我们的目标是融合一块土地的灌溉和产量数据。无论我们如何定义数据对象以服务于我们的分析目的,到最后,我们将只分析一块土地。 因此,不同的假设集,使数据源被融合,可能会导致不同数量的数据对象,但这块土地保持不变。然而,让我们想象一下,我们有不止一块土地,我们想要整合其数据。 这将成为通过添加数据对象的数据整合。 到目前为止,我们已经了解了数据整合的不同方面。我们已经了解了它是什么以及它的目标。我们还涵盖了数据整合的两个方向。接下来,我们将了解到数据整合和数据融合的六个挑战。之后,我们将看一下那些经常出现的挑战的例子。

2 关于数据融合和整合的频繁挑战

虽然每项数据集成任务都是独一无二的,但有一些挑战是你会经常面对的。在本章中,你将了解这些挑战,并通过实例,掌握处理这些挑战的技巧。首先,让我们来了解一下每个挑战。然后,通过以其中一个或多个为特征的例子,我们将掌握处理它们的宝贵技能。

2.1 挑战1--实体识别

当通过添加属性来整合数据源时,可能会出现实体识别挑战--或者如文献中所说的实体识别问题。这个挑战是,所有数据源中的数据对象都是相同的现实世界的实体,具有相同的数据对象定义,但是由于数据源中的独特标识符,它们不容易连接起来。例如,在数据整合的例子部分,销售部门和市场部门没有对所有的客户使用一个中央客户唯一标识符。由于这种数据管理的缺乏,当他们想整合数据时,他们必须弄清楚数据源中的哪个客户是哪个。

2.2 挑战2 - 不明智的数据收集

这种数据整合挑战的发生,正如其名称所示,是由于不明智的数据收集。例如,不使用集中式数据库,而是将不同数据对象的数据存储在多个文件中。我们在第9章《数据清理第一级--清理表》中也谈到了这个挑战。在继续阅读之前,请回过头来回顾一下例1--不明智的数据收集,。这个挑战既可以看作是第一层次的数据清理,也可以看作是数据整合的挑战。不管怎么说,在这些情况下,我们的目标是确保数据被整合到一个标准的数据结构中。这种类型的数据整合挑战发生在数据对象被添加的时候。

2.3 挑战3--索引的格式不匹配

当我们开始通过添加属性来整合数据源时,我们将使用pandas DataFrame .join()函数来连接具有相同索引的两个DataFrames的行。要使用这个有价值的函数,整合的DataFrames需要有相同的索引格式,否则,该函数将无法连接行。例如,下图显示了将两个DataFrames结合起来的三次尝试:temp_dfelectric_df。 temp_df包含2016年的每小时温度(temp),而electric_df携带同年的每小时电力消耗(consumption)。由于索引不匹配的格式挑战,前两次尝试(最上面一次和中间一次)是不成功的。例如,考虑顶部的尝试;虽然两个DataFrames都有Date和Time的索引,并且都显示相同的Date和Time,但尝试.join()函数会产生一个 "不能连接,没有重叠的索引名称 "的错误。发生了什么?整合的尝试不成功,因为两个DataFrames的索引格式不一样。

image.png

在上图中,虽然中间的尝试比上面的好,但还是不成功的。密切注意,看看你是否能弄清楚为什么在积分的输出中会有这么多的NaN。

2.4 挑战4--聚合不匹配

在通过添加属性来整合数据源时,会出现这种挑战。当整合时间间隔不一致的时间序列数据源时,这种挑战就出现了。例如,如果要整合下图中的两个DataFrames,我们不仅要解决索引不匹配格式的挑战,还需要面对聚合不匹配的挑战。这是因为temp_df携带的是每小时的温度数据,但electric_df携带的是每半小时的耗电量。

image.png

为了应对这一挑战,我们将不得不重组一个来源或两个来源,使它们具有相同的数据聚合水平。我们很快就会看到这一点,所以现在,让我们来介绍另一个挑战。

2.5 挑战5 - 重复的数据对象

当我们通过添加数据对象来整合数据源时,就会出现这种挑战。当数据源包含的数据对象也在其他数据源中时,当数据源被整合时,在整合后的数据集中会出现相同数据对象的重复。 例如,设想一家提供不同种类医疗服务的医院。在一个项目中,我们需要收集医院里所有病人的社会经济数据。 想象中的医院没有一个集中的数据库,所以所有的部门都要负责返回一个包含他们提供服务的所有病人的数据集。 在整合了来自不同部门的所有数据集后,你应该预料到,有多行是不得不接受医院不同部门护理的病人。

2.6 挑战6--数据冗余

这个挑战的名字似乎也适合前一个挑战,但在文献中,数据冗余这个词被用于一种独特的情况。与前一个挑战不同,这个挑战可能是在你通过添加属性来整合数据源时面临的。顾名思义,在数据整合后,一些属性可能是冗余的。这种冗余可能是浅层的,因为有两个标题不同但数据相同的属性。或者,它可能是更深的。在更深层次的数据冗余情况下,冗余的属性没有相同的标题,其数据也不与其他属性之一相同,但冗余属性的值可以从其他属性中导出。 例如,在将数据源整合为一个客户数据集后,我们有以下七个属性:年龄、平均订单金额、距离上次访问的天数、每周访问频率、每周购买金额和满意度评分。如果我们使用所有七个属性来对客户进行聚类,我们就犯了一个数据冗余的错误。在这里,每周访问频率、每周购买金额和平均订单金额的属性是不同的,但是每周购买金额的值可以从每周访问频率和平均订单金额中得出。这样做,无意中,我们会在聚类分析中给予客户访问和购买金额的信息更多的权重。 我们应该根据分析目标和数据分析工具来处理数据冗余的挑战。例如,如果我们采用决策树算法来预测顾客的满意度,我们就不需要担心数据冗余的问题。这是因为决策树算法只使用有助于其性能的属性。 然而,如果用线性回归来完成同样的任务,如果你不去掉每周购买的美元,你会有一个问题。这是因为同样的信息出现在多个属性中会混淆线性回归的结果。这有两个原因。

  • • 首先,线性回归算法必须使用所有的独立属性,因为它们被输入。

  • • 第二,该算法需要想出一套权重,用于所有独立属性的所有数据对象,而且是同时进行。在回归分析中,这种情况被称为勾稽关系,应该加以避免。现在我们已经了解了数据整合的这六个常见挑战,让我们来看看一些具有其中一个或一些挑战的例子。

3 例1(挑战3和4)

在这个例子中,我们有两个数据来源。第一个是从当地的电力供应商那里检索到的,其中有电力消耗(电力数据2016_2017.csv),而另一个是从当地气象站检索的,包括温度数据(Temperature 2016.csv)。我们想看看是否能想出一个可视化的方法,来回答用电量是否以及如何受到天气的影响。 首先,我们将使用pd.read_csv()将这些CSV文件读入两个名为electric_dftemp_df的pandas DataFrames。将数据集读入这些DataFrames后,我们将查看它们以了解其数据结构。你会注意到以下问题。- electric_df的数据对象定义是15分钟内的耗电量,但temp_df的数据对象定义是每1小时的温度。这表明我们必须面对数据集成的聚合不匹配挑战(挑战4)。

import numpy as np
import matplotlib.pyplot as plt
import pandas as pd
import seaborn as sns

electric_df = pd.read_csv('Electricity Data 2016_2017.csv')
temp_df = pd.read_csv('Temperature 2016.csv')
electric_df
image.png
temp_df
image.png
  • • temp_df只包含2016年的数据,而electric_df包含2016年和2017年的部分数据。

  • • temp_df和electric_df都没有索引,可以用来连接两个DataFrames的数据对象。这表明,我们还将不得不面对索引不匹配的格式化的挑战(挑战3)。

为了克服这些问题,我们将执行以下步骤。

  1. 1. 从electric_df中删除2017年的数据对象。下面的代码使用布尔掩码和.drop()函数来完成。

BM = electric_df.Date.str.contains('2017')
dropping_index = electric_df[BM].index
electric_df.drop(index = dropping_index,inplace=True)
electric_df

在成功运行前面的代码后,检查 electric_df 的状态。 你会看到2016年的electric_df是每半小时记录一次。

image.png
  1. 1. 从时间属性中为electric_df添加一个名为Hour的新列。下面的代码通过使用.apply()函数,在一行代码中完成了这一工作。

electric_df['Hour'] = electric_df.Time.apply(lambda v: '{}:00'.format(v.split(':')[0]))
electric_df
image.png
  1. 1. 创建一个新的数据结构,其数据对象的定义是每小时的用电量。下面的代码使用.groupby()函数来创建 integrate_srPandas integrate_sr系列是一个权宜之计的数据结构,将在后面的步骤中用于整合。

integrate_sr = electric_df.groupby(['Date','Hour']).Consumption.sum()
integrate_sr
image.png

这里要问的一个好问题是,为什么我们要使用.sum()聚合函数而不是.mean()?原因是数据的性质。一小时的耗电量是其半小时的耗电量的总和。

  1. 1. 在这一步,我们将把注意力转向temp_df。我们将从Timestamp向temp_df添加日期和小时列。下面的代码通过应用一个显式函数来实现这一目的。首先,我们将创建该函数。

def unpackTimestamp(r):
    ts = r.Timestamp
    date,time = ts.split('T')
    hour = time.split(':')[0]
    year,month,day = date.split('-')
    
    r['Hour'] = '{}:00'.format(int(hour))
    r['Date'] = '{}/{}/{}'.format(
        int(month),int(day),year)
    return(r)

然后,我们将对temp_df数据框架应用该函数。

temp_df = temp_df.apply(unpackTimestamp,axis=1)

在成功运行前面的代码块后,检查temp_df的状态,然后再转到下一个步骤。

  1. 1. 对于temp_df,设置日期和小时属性作为索引,然后放弃时间戳列。下面的代码在一行中完成了这个工作。

temp_df = temp_df.set_index(['Date','Hour']).drop(columns=['Timestamp'])
temp_df
image.png

再次,在成功运行前面的代码后,检查temp_df的状态,然后再进入下一步。

  1. 1. 在所有这些重新格式化和重组之后,我们已经准备好使用.join()来整合这两个源。困难的部分是使用.join()之前的内容。应用这个函数就像应用它一样简单。自己看吧。

integrate_df =temp_df.join(integrate_sr)
integrate_df
image.png

请注意,我们在步骤3中把 integrate_sr作为一个权宜之计的数据结构。 像往常一样,在继续阅读之前,花点时间研究一下 integrate_df 的样子。

  1. 1. 重置 integrate_df的索引,因为我们不再需要索引用于整合,也不需要这些值用于可视化目的。运行下面的代码就可以解决这个问题了。

integrate_df.reset_index(inplace=True)
  1. 1. 创建一个全年耗电量的线图,其中温度这个维度是用颜色添加到线图中的。这个可视化图如图12.5所示,是用我们在本书中学到的工具创建的。下面的代码创建了这个可视化。

days = integrate_df.Date.unique()

max_temp, min_temp = integrate_df.temp.max(), integrate_df.temp.min()
green =0.1

plt.figure(figsize=(20,5))

for d in days:
    BM = integrate_df.Date == d
    wdf = integrate_df[BM]
    
    average_temp = wdf.temp.mean()
    red = (average_temp - min_temp)/ (max_temp - min_temp)
    blue = 1-red
    clr = [red,green,blue]
    plt.plot(wdf.index,wdf.Consumption,c = clr)
BM = (integrate_df.Hour =='0:00') & (integrate_df.Date.str.contains('/28/'))
plt.xticks(integrate_df[BM].index,integrate_df[BM].Date,rotation=90)
plt.grid()
plt.margins(y=0,x=0)
plt.show()
image.png

前面的代码将许多部分结合在一起,使下面的可视化得以实现。该代码最重要的方面如下。

  • • 该代码创建了天数列表,其中包含来自 integrate_df 的所有独特日期。总的来说,前面的代码是通过天数列表的循环,对于每一个独特的日子,都会画出耗电量的线图,并与前后的日子相加。每一天的线图的颜色是由该天的温度平均值决定的,也就是temp.mean()。

  • • 可视化中的颜色是根据RGB颜色代码创建的。RGB代表红、绿、蓝。所有的颜色都可以通过使用这三种颜色的组合来创建。你可以指定你想要的每种颜色的数量,Matplotlib将为你产生这种颜色。对于Matplotlib来说,这些颜色的值可以从0到1。在这里,我们知道,当绿色被设置为0.1,而红色和蓝色之间有一个蓝色=1-红色的关系,我们可以创建一个红色-蓝色的颜色光谱,可以很好地代表热和冷的颜色。该光谱可以用来显示更热和更冷的温度。这已经通过计算温度的最大值和最小值(使用max_temp和min_ temp),并在适当的时候计算出clr的三个红、绿、蓝元素,作为颜色值传递给plt.plot()函数。

  • • 一个布尔掩码(BM)和plt.xticks()被用来在x轴上包括每个月的28号,这样我们就不会有一个杂乱的x轴了。

现在,让我们把注意力转移到前述图表中显示的分析值。我们可以看到温度和消费之间的明显关系;随着天气变冷,电力消耗也会增加。 如果没有整合这两个数据源,我们就无法画出这个可视化的图。通过体验这个可视化的附加分析价值,你也可以体会到数据整合的价值,看到必须处理挑战3--索引不匹配的格式和挑战4--聚合不匹配的意义。

4 例2(挑战2和3)

在这个例子中,我们将使用Taekwondo_Technique_Classification_Stats.csvtable1.csv数据集。这些数据集由2020装甲公司(https://2020armor.com/)收集,该公司是有史以来第一个电子评分背心和应用程序的供应商。这些数据包括六名跆拳道运动员的传感器性能读数,他们有不同的经验和专业知识水平。我们想看看运动员的性别、年龄、体重和经验是否会影响他们在执行以下技术时所能产生的影响程度。

  • • 回旋/圆踢(R)Roundhouse/Round Kick (R)

  • • 后踢(B)Back Kick (B)

  • • 切踢(C)Cut Kick (C)

  • • 冲拳(P)Punch (P)

数据被存储在两个独立的文件中。我们将使用pd.read_csv()来读取table1.csv读入athlete_dfTaeKwondo_Technique_Classification_Stats.csvunknown_df中。在继续阅读之前,花点时间研究一下athlete_dfunknown_df,评估它们的状态,以进行分析。

athlete_df = pd.read_csv('https://raw.githubusercontent.com/PacktPublishing/Hands-On-Data-Preprocessing-in-Python/main/Chapter12/Table1.csv')
athlete_df
image.png
unknown_df = pd.read_csv('https://raw.githubusercontent.com/PacktPublishing/Hands-On-Data-Preprocessing-in-Python/main/Chapter12/Taekwondo_Technique_Classification_Stats.csv')
unknown_df
image.png

经过分析,我们会发现,为athlete_df选择的数据结构很简单易懂。athlete_df的数据对象的定义是运动员,这意味着每一行代表一个跆拳道运动员_df。然而,unknown_df的数据结构并不容易理解,有些令人困惑。其原因是,尽管使用了一个非常常见的数据结构--表,但它并不合适。正如我们在第3章 "数据--它到底是什么?","最通用的数据结构--表 "中所讨论的那样,我们知道,将表维系在一起的粘合剂是对数据对象的可理解的定义。因此,在这个例子中,我们将面临的主要数据整合挑战是挑战2--不明智的数据收集。 当我们面临不明智的数据收集挑战时,为了整合数据,类似于我们在第九章《数据清理第一级--清理表》中的例子1--不明智的数据收集,我们需要数据结构及其设计来支持以下两个事项。

  • • 该数据结构可以包括所有文件的数据。

  • • 该数据结构可用于上述分析。

正如我们所讨论的,athlete_df数据集是简单易懂的,但unknown_df中的信息包括什么?把二和二放在一起后,我们会意识到,六名跆拳道运动员表演的传感器读数都在athlete_df中。通过研究unknown_df,我们还意识到,每个运动员都对上述四种技术各进行了五次。这些技术在unknown_df中用字母R、B、C和P进行编码;R代表回旋拳,B代表后踢,C代表切踢,P代表冲拳。此外,我们可以看到,每项技术由每个运动员执行五次。 运行以下代码将创建一个名为performance_df的空pandas数据框架。这个数据集的设计是为了让athlete_dfunknown_df都能被整合到其中。 我们为performance_df设计的行数(n_rows)是1减去unknown_df的列数:len(unknown_df.columns)-1。当我们要填满performance_df时,我们会看到为什么会是这样的情况。

designed_columns = ['Participant_id','Gender','Age','Weight','Experience','Technique_id','Trial_number','Average_read']
n_rows = len(unknown_df.columns)-1
performance_df = pd.DataFrame(index=range(n_rows),columns =designed_columns)
performance_df
image.png

由于数据集的收集不明智,因此我们不能在此处使用 .join() 等简单函数进行数据集成。相反,我们需要使用循环来遍历 unknown_df 和 player_df 的许多记录,并逐行、有时逐个单元格地填写 performance_df。 以下代码将同时使用 player_df 和 unknown_df 来填充 performance_df。让我们开始吧:

  1. 1. 首先,我们需要对atthlete_df进行一些一级数据清理,以便在循环中访问这个数据框架变得更容易。下面的代码为athlete_df处理了这些清理步骤。

athlete_df.set_index('Participant ID',inplace=True)
athlete_df.columns = ['Sex''Age''Weight''Experience''Belt']
athlete_df
image.png

研究一下运行前面的代码后athlete_df的状态,确保你在继续阅读之前理解每一行代码的作用。

  1. 1. 现在athlete_df已经比较干净了,我们可以创建并运行循环,将performance_df填满。如下面的截图所示,该循环会遍历unknown_df中的所有列。除了unknown_df中的第一列,每一列都包含performance_df中的一条记录的信息。因此,在循环通过unknown_df的列的每一次迭代中,performance_df中的一行将被填充。为了填满performance_df中的每一行,数据必须来自athlete_df和unknown_df。我们将使用我们所知道的来自athlete_df和unknown_df的结构。

techniques = ['R','B','C','P']
index = 0
for col in unknown_df.columns:
    if(col[0in techniques):
        performance_df.loc[index,'Technique_id'] = col[0]
        performance_df.loc[index,'Trial_number'] = unknown_df[col][1]
        
        P_id = unknown_df[col][0]
        performance_df.loc[index,'Participant_id'] = P_id
        performance_df.loc[index,'Gender'] = athlete_df.loc[P_id].Sex
        performance_df.loc[index,'Age'] = athlete_df.loc[P_id].Age
        performance_df.loc[index,'Weight'] = athlete_df.loc[P_id].Weight
        performance_df.loc[
            index,'Experience'] = athlete_df.loc[P_id].Experience
        BM = unknown_df[col][2:].isna()
        performance_df.loc[
            index,'Average_read'] = unknown_df[
            col][2:][~BM].astype(int).mean()
        index +=1
performance_df
image.png

注意! 在这一章中,会有一些非常大的代码的例子,比如前面的截图所示。由于这段代码的大小,我们不得不包括一个截图,而不是一个可复制的代码块。要复制这段代码,请看本书GitHub仓库中的chapter12文件夹。

  1. 1. 成功运行前面截图中的代码后,performance_df将被填满。在继续阅读之前,打印performance_df以检查其状态。

  2. 2. 现在,数据整合已经完成,我们可以把注意力放在数据分析的目标上。下面的代码创建了一个基于性别、年龄、体重和经验的Average_read的箱形图。

select_attributes = ['Gender','Age','Experience','Weight']
for i,att in enumerate(select_attributes):
    plt.subplot(2,2,i+1)
    sns.boxplot(data = performance_df,
                y='Average_read', x=att)
plt.tight_layout()
plt.show()
image.png

在上图中,我们可以看到Average_read与性别、年龄、经验和体重之间有意义的关系。简而言之,这些属性可以改变运动员所执行的技术的影响。例如,我们可以看到,随着运动员经验的增加,运动员所实施的技术的影响也会增加。 我们还可以看到一个令人惊讶的趋势:女运动员所实施的技术的影响明显高于男运动员的影响。在看到这个令人惊讶的趋势之后,让我们再来看看atthlete_df。我们会发现,数据中只有一个女运动员,所以我们不能指望这个可视化的趋势。 在我们继续下一个数据整合的例子之前,让我们玩一玩,创建具有更高维度的可视化。下面的代码创建了多个箱形图,包括Average_read、Experience和Technique_id维度。

sns.boxplot(data = performance_df, y='Average_read', x='Experience',hue='Technique_id')
image.png

在继续阅读之前,看看前面的图,看看你是否能发现更多的关系和模式。 现在,让我们把注意力放到下一个例子上。系好安全带--下一个例子将是一个复杂的例子,有许多不同的方面。

5 例3(挑战1、3、5和6)。

在这个例子中,我们想弄清楚是什么让一首歌曲上升到Billboard(https://www.billboard.com/charts/hot-100)的前10名,并在那里停留至少5周。公告牌杂志每周都会发布一个排行榜,根据美国的销量、电台播放量和在线流媒体对流行歌曲进行排名。我们将整合三个CSV文件--billboardHot100_1999-2019.csv、songAttributes_1999-2019.csv和artistDf.csv,从https://www.kaggle.com/danield2255/data-on-songs-from-billboard-19992019,来完成这一工作。 这将是一个很长的例子,其中有许多片断都在一起。在这样的数据整合挑战中,你如何组织你的思想和工作是非常重要的。因此,在继续阅读之前,花一些时间了解这三个数据源,并形成一个计划。 这将是一个非常有价值的实践。 现在你已经有机会思考你会如何去做了,让我们一起做吧。这些数据集似乎是从不同的来源收集的,所以这三个数据文件中的任何一个或全部都可能有重复的数据对象。将文件分别读入billboard_df、SongAttributes_df和artist_df后,我们将检查其中是否有重复的数据对象。这就是在处理挑战5--重复的数据对象。

5.1 检查重复的数据对象

我们将不得不对每个文件都这样做。我们将从billboard_df开始,然后再对songAttributes_df和artist_df进行同样的处理。

检查billboard_df中是否有重复的内容

下面的代码将billboardHot100_1999-2019.csv文件读入Billboard_df,然后创建一个名为wsr的pandas系列。wsr这个名字是Working SeRies的简称。正如我之前提到的,当我需要一个临时的DataFrame或系列来做一些分析时,我往往会创建一个wdf(工作数据框架)或wsr。 在这种情况下,wsr用于创建一个新的列,该列是艺术家、姓名和周列的组合,所以我们可以用它来检查数据对象是否唯一。 这种多栏检查的原因很明显,对吗?可能有不同艺术家的同名歌曲;每个艺术家可能有不止一首歌曲;或者,同一首歌可能有不同的周报。所以,为了检查整个billboard_df的数据对象的唯一性,我们需要这个列。

billboard_df = pd.read_csv('billboardHot100_1999-2019.csv')
wsr = billboard_df.apply(lambda r: '{}-{}-{}'
                         .format(r.Artists,r.Name,r.Week),axis=1)
wsr.value_counts()
image.png

运行前面的代码后,输出显示所有的数据对象都出现了一次,除了50 Cent在2005-09-14周的歌曲Outta Control。运行billboard_df.query("Artists == '50 Cent' and Name=='Outta Control' and Week== '2005-09-14'")将过滤掉这两个数据对象。下面的屏幕截图显示了运行这段代码的结果。

billboard_df.query("Artists == '50 Cent' and Name=='Outta Control' \
                   and Week== '2005-09-14'"
)
image.png

在这里,我们可以看到这两行几乎是相同的,没有必要同时拥有它们。我们可以使用.drop()函数来删除这两行中的一行。这在下面一行代码中显示。

billboard_df.drop(index = 67647,inplace=True)

在成功运行前面的一行代码后,似乎什么也没有发生。 这是由于 inplace=True,它使 Python 在原地更新 DataFrame 而不是输出一个新的 DataFrame。 现在我们已经确定了bilboard_df中每一行的唯一性,让我们继续前进,对songAttributes_df做同样的事情。

检查songAttributes_df中是否有重复的内容

我们将使用非常类似的代码和方法来查看 songAttributes_df中是否有重复的内容。下面的代码已经为新的数据框架做了改动。 首先,代码将 songAttributes_1999-2019.csv 文件读入 songAttributes_df,然后创建新的列,并使用 .value_counts() 检查是否有重复的内容,这是每个 pandas 系列的一个函数。

songAttribute_df = pd.read_csv('songAttributes_1999-2019.csv')
wsr = songAttribute_df.apply(
    lambda r: '{}---{}'.format(r.Artist,r.Name),axis=1)
wsr.value_counts()
image.png

运行前面的代码后,我们会看到许多歌曲在 songAttributes_df 中有重复的行。 我们需要找出造成这些重复的原因。我们可以过滤掉一些歌曲的重复行,然后研究它们。例如,从顶部开始,我们可以分别运行以下几行代码来研究它们的输出。

songAttribute_df.query("Artist == 'Jose Feliciano' and Name == 'Light My Fire'")
image.png
songAttribute_df.query("Artist == 'Dave Matthews Band' and Name == 'Ants Marching - Live'")
image.png

在研究了这些代码的输出后,我们会意识到,有两个可能的原因导致重复的存在。

  • • 首先,同一首歌曲可能有不同的版本。

  • • 第二,数据收集过程可能是由不同的资源完成的。

我们的研究还表明,这些重复的属性值虽然不完全相同,但却非常相似。 为了能够进行这种分析,我们需要每首歌曲只有一行。因此,我们需要删除所有有重复的歌曲的行,或者将它们集中起来。 两者都可能是正确的行动方案,这取决于具体情况。在这里,我们将删除所有的重复数据,除了第一条。下面的代码循环浏览 songAttributes_df 中有重复数据对象的歌曲,并使用 .drop() 删除所有重复的数据对象,除了第一个对象。首先,代码创建了doFrequencies(do是数据对象的简称),它是一个pandas系列,显示songAttributes_df中每首歌曲的频率,并循环浏览doFrequencies中频率高于1的元素。

songAttribute_df = pd.read_csv('songAttributes_1999-2019.csv')
wsr = songAttribute_df.apply(
    lambda r: '{}---{}'.format(r.Name,r.Artist),axis=1)
doFrequencies = wsr.value_counts()

BM = doFrequencies>1

for i,v in doFrequencies[BM].iteritems():
    [name,artist] = i.split('---')
    BM = ((songAttribute_df.Name == name) &
          (songAttribute_df.Artist == artist))
    wdf = songAttribute_df[BM]
    
    dropping_index = wdf.index[1:]
    songAttribute_df.drop(index = dropping_index, 
                          inplace=True)

如果你尝试运行前面的代码,你会看到它需要很长的时间。在我的电脑上花了大约30分钟。在这种情况下,在代码中添加一些元素是很好的,可以让用户知道代码已经运行了多少,还需要多少时间。下面的代码与前面的代码相同,但增加了一些元素,以建立一个报告运行时间进度的机制。 我建议改运行下面的代码。但在这样做之前,请比较这两个选项,并研究如何添加报告机制。不要忘记,在你运行下面的代码之前,你需要导入时间模块。时间模块是一个优秀的模块,它允许我们处理时间和时差。

import time

songAttribute_df = pd.read_csv('songAttributes_1999-2019.csv')
wsr = songAttribute_df.apply(lambda r: '{}---{}'
                             .format(r.Name,r.Artist),axis=1)
doFrequencies = wsr.value_counts()

BM = doFrequencies>1
n_totalSongs = sum(BM)
print('Total processings: ' + str(n_totalSongs))

t = time.time()
i_progress = 0
for i,v in doFrequencies[BM].iteritems():
    [name,artist] = i.split('---')
    BM = ((songAttribute_df.Name == name) & 
          (songAttribute_df.Artist == artist))
    
    wdf = songAttribute_df[BM]
    dropping_index = wdf.index[1:]
    songAttribute_df.drop(index = dropping_index, inplace=True)
    
    i_progress +=1
    if(i_progress%500==0):
        print('Processed: ' + str(i_progress))
        process_time = time.time() - t
        print('Elapsed: ' + str(round(process_time,1)) + ' s')
        estimate_finish = round((n_totalSongs-i_progress) *
                                (process_time/500)/60,1)
        
        print('To finish: ' + str(estimate_finish)+ 'mins')
        t = time.time()
        print('------------------------------------')
image.png

一旦你成功地运行了前面的代码(这将需要一段时间),songAttributes_df将不会出现数据对象重复的问题。 接下来,我们将检查artist_df是否包含重复的数据对象,如果有的话,就解决它们。

wsr = songAttribute_df.apply(lambda r: '{}---{}'
                             .format(r.Artist,r.Name),axis=1)
doFrequencies = wsr.value_counts()
doFrequencies
image.png

检查artist_df中是否有重复的内容

检查artisit_df中数据对象的唯一性比我们之前看的两个DataFrames要容易。其原因是,artist_df中只有一个识别列。songAttribute_df和billboard_df则有两个和三个识别列。 下面的代码将artistDf.csv读入artisit_df,并使用.value_counts()函数来检查artisit_df中的所有行是否唯一。

artist_df = pd.read_csv('artistDf.csv')
artist_df.Artist.value_counts()
image.png

运行前面的代码并研究其结果后,你会发现有两行代表艺术家Reba McEntire。运行artist_df.query("Artist == 'Reba McEntire'")将过滤掉这两行。下面的屏幕截图显示了运行这段代码的结果。

artist_df.query("Artist == 'Reba McEntire'")
image.png

在这里,我们可以看到这两行是一样的,没有必要同时拥有它们。下面一行代码使用.drop()函数来删除这两行中的一行。

artist_df.drop(index = 716, inplace=True)

在成功运行前面的一行代码后,似乎什么也没有发生。 这是由于 inplace=True,它使 Python 在原地更新 DataFrame 而不是输出一个新的 DataFrame。 干得好! 现在,我们知道,我们所有的 DataFrame 只包含唯一的数据对象。我们可以用这个知识来解决数据整合的挑战性任务。 如果我们一开始就把目标定在这里,那就更好了。在下一节中,我们将设想并创建我们希望在数据集成过程结束时拥有的DataFrame的结构。

5.2 为数据整合的结果设计结构

由于涉及到两个以上的数据源,我们必须对数据整合的结果有一个愿景。做到这一点的最好方法是设想并创建一个数据集,其数据对象的定义及其属性有可能回答我们的分析问题,同时,可以由我们拥有的数据源来填充。 下面的截图显示了可以创建一个包含所列特征的数据集的代码。数据对象的定义是歌曲,而属性可以用三个DataFrames之一来填写。一旦songIntegrate_df被填入,它可以帮助我们回答这样一个问题:是什么让一首歌在Billboard上一路上升到前10名,并在那里停留至少5周。

songIntegrate_df = pd.DataFrame(
    columns = ['Name''Artists''Top_song''First_date_on_Billboard',
               'Acousticness''Danceability''Duration''Energy',
               'Explicit''Instrumentalness''Liveness''Loudness',
               'Mode''Speechiness''Tempo''TimeSignature''Valence',
               'Artists_n_followers''n_male_artists''n_female_artists',
               'n_bands''artist_average_years_after_first_album',
               'artist_average_number_albums'])
songIntegrate_df
image.png

songIntegrate_df中大多数设想的属性都是直观的。我们来看看那些可能不那么明显的属性。

  • • Top_song。一个二进制属性,描述歌曲是否在Billboard的前10首歌曲中至少停留了5周。

  • • First_date_on_Billboard。该歌曲在Billboard上的第一个日期。

  • • 原声性、可舞蹈性、持续时间、能量、明确性、乐器性、生动性、响度、模式、言语性、节奏、时间特征和价值是歌曲的艺术属性。这些属性将从songAttribute_df整合到songIntegrate_df

  • • Artists_n_followers。艺术家的或艺术家在社交媒体上的追随者数量。如果有一个以上的艺术家,将使用他们的追随者数量的总和。

  • • n_male_artistsn_female_artists是显示艺术家性别的属性。如果一个女艺术家制作了这首歌,他们的值将分别为0和1。如果有两位男性艺术家制作了这首歌,他们的值将分别为2和0。

  • • n_bands: 参与制作该歌曲的乐队数量。

  • • artist_average_years_after_first_album(艺术家平均年数)试图捕捉艺术家的从业经验。如果一个艺术家创作了这首歌曲,那么将使用一个单一的数值,而当超过一个艺术家参与时,将使用一个平均值。这些数值将根据First_date_on_Billboard来计算。

  • • artist_average_number_albums(艺术家平均数)也试图捕捉艺术家的从业经验。与前一个属性类似,如果一个艺术家创作了这首歌曲,那么将使用一个单一的数值,而当涉及到一个以上的艺术家时,将使用一个平均数值。

前四个属性将使用billboard_df填写,后六个属性将使用artist_df填写,其余的将使用songAttribute_df填写。 注意,First_date_on_Billboard属性将被临时创建。它将从billboard_df中填入,这样,当我们开始从artist_df中填入时,我们就可以用First_date_on_Billboard来计算艺术家的平均年限(average_years_ after_first_album)。 在我们开始从这三个来源填入songIntegrate_df之前,让我们看看是否有可能必须从songIntegrate_df中删除一些歌曲。这可能成为不可避免的,因为我们可能需要的每首歌曲的文件信息可能不存在于其他资源中。因此,本例中的其余小节将如下。

  • • 从billboard_df填充songIntegrate_df

  • • 从songAttribute_df填充songIntegrate_df

  • • 删除数据不完整的数据对象

  • • 从artist_df填充songIntegrate_df

  • • 检查songIntegrate_df的状态

  • • 进行分析

看来我们有很多地方要做,所以我们开始吧。我们将从使用billboard_df来填充songIntegrate_df开始。

5.3 从billboard_df填充songIntegrate_df

在填写SongIntegrate_df的这一部分,我们将填写前四个属性。名称、艺术家、顶级歌曲和首次上榜日期。填充前两个属性比较简单;后两个属性需要对billboard_df中的一些行进行计算和汇总。 将数据从billboard_df填入SongIntegrate_df的挑战是双重的。首先,两个DataFrames中的数据对象的定义是不同的。我们把songIntegrate_df中数据对象的定义设计为歌曲,而billboard_df中数据对象的定义是歌曲的广告牌排名的每周报告。其次,由于billboard_df对数据对象的定义更为复杂,它也需要更多的识别属性来区分独特的数据对象。对于billboard_df来说,三个识别属性是名称、艺术家和星期,但对于songIntegrate_df来说,我们只有名称和艺术家。 songIntegrate_df数据框架是空的,不包含任何数据对象。由于我们为这个DataFrame考虑的数据对象的定义是歌曲,所以最好在songIntegrate_df中为billboard_df中的所有独特的歌曲分配一个新行。 下面的代码使用嵌套的循环遍历billboard_df中所有独特的歌曲,以填充songIntegrate_df。第一个循环遍历了所有独特的歌曲名称,所以每次迭代都会处理一个独特的歌曲名称。由于可能有不同的歌曲有相同的歌名,代码在第一个循环中做了以下工作。

  1. 1. 首先,它将过滤所有带有迭代的歌曲名称的行。

  2. 2. 其次,它将找出所有有迭代中的歌曲名称的艺术家。

  3. 3. 第三,它将查看我们在第二步中确认的所有艺术家,并且在第二个循环的每个迭代中,我们将向songIntegrate_df添加一行。

为了向songIntegrate_df添加一行,下面的代码使用了.append()函数。这个函数要么接受一个pandas系列,要么接受一个Dictionary,将其添加到一个DataFrame中。在这里,我们使用的是一个字典;这个字典将有四个键,这四个键是songIntegrate_df的四个属性--Name、Artists、Top_song、First_date_on_Billboard--我们打算从billboard_df中填写。填充姓名和艺术家很容易,因为我们需要做的就是插入billboard_df中的值。然而,我们需要进行一些计算来计算Top_song和First_date_on_Billboard的值。 研究下面的代码,试着理解代码中试图计算这两个属性的部分背后的逻辑。对于Top_song,试着看看你是否能把逻辑和我们要做的事情联系起来。回到本例中的第一段。对于First_date_on_Billboard,代码对billboard_df做了一些假设。看看你能不能发现这个假设是什么,然后调查这个假设是否可靠。 现在,是时候让你试一试这段代码了。在你按下运行键之前,请注意:它可能需要一段时间才能完成。它不会像前面的代码那样冗长地运行,但也不会是瞬间的。

SongNames = billboard_df.Name.unique()

for i, song in enumerate(SongNames):
    BM = billboard_df.Name == song
    wdf = billboard_df[BM]
    
    Artists = wdf.Artists.unique()
    for artist in Artists:
        BM = wdf.Artists == artist
        wdf2 = wdf[BM]
        
        topsong = False
        
        BM = wdf2['Weekly.rank'] <=10
        if(len(wdf2[BM])>=5):
            topsong = True
            
        first_date_on_billboard = wdf2.Week.iloc[-1]
        dic_append = {'Name':song,'Artists':artist,
                      'Top_song':topsong,
                      'First_date_on_Billboard'
                      first_date_on_billboard}
        
        songIntegrate_df = songIntegrate_df.append(
            dic_append, ignore_index=True)

成功运行前面的代码后,打印SongIntegrate_df来研究DataFrame的状态。

songIntegrate_df
image.png

我们刚才面临的挑战和在这里解决的问题可以归类为挑战3--索引不匹配的格式化。这个特殊的挑战比较困难,因为我们不仅有不同的索引格式,而且还有不同的数据对象的定义。为了能够进行数据整合,我们不得不避免将识别属性声明为索引。为什么呢?因为那对我们的数据整合目标没有帮助。然而,不得不这样做也迫使我们把事情掌握在自己手中,使用循环而不是更简单的函数,如我们在例1(挑战3和4)和例2(挑战2和3)中看到的.join()。 接下来,我们将从songAttribute_df中填充songIntegrate_df的一些剩余属性。这样做对我们的挑战有些不同;我们将不得不处理挑战1--实体识别。

5.4从songAttribute_df填充songIntegrate_df

在数据整合的这一部分,我们必须考虑的挑战是实体识别。 虽然songIntegrate_df和songAttribute_df的数据对象的定义是相同的,即歌曲,但在两个数据框架中区分独特数据对象的方式是不同的。差异的关键在于 songIntegrate_df.Artists 和 songAttribute_df.Artist 属性;注意艺术家的复数和艺术家的单数。你会看到,有多个艺术家的歌曲在这两个DataFrames中的记录是不同的。然而,在songIntegrate_df中,一首歌曲的所有艺术家都包括在songIntegrate_df.Artists属性中,用逗号(,)分开;在songAttribute_df中,只有主要艺术家被记录在songAttribute_df.Artist中,如果其他艺术家参与了一首歌曲,他们被添加到songAttribute_df.Name中。这使得从两个DataFrames中识别相同的歌曲变得非常困难。所以,我们在这里进行数据整合之前,需要有一个计划。 下表显示了同样的歌曲进入我们两个来源的五种不同情况。让我们来回答关于这五种情况的两个问题。 首先,我们是如何得出这五种情况的?这是个很好的问题。在处理实体识别的挑战时,你需要研究数据的来源,并弄清楚如何与来源中的识别属性合作。然后,你可以用计算机将那些属于同一实体但编码方式不一样的行连接起来。所以,这个问题的答案是,我们刚刚研究了这两个来源,足以意识到这五种情况的存在。 第二,我们该如何处理这些情况?回答这个问题很简单。我们将利用它们来起草一些代码,将两个来源的可识别歌曲连接起来,以连接和整合数据集。

image.png

下面的代码相当长,它使用了前述图表中提取的五种情况以及我们在本书中掌握的所有其他编码能力来执行整合任务。这段代码循环浏览SongIntegrate_df的行,并搜索SongAttribute_df中任何列出歌曲的行。该代码采用了我们提取的五种情况来创建前面的图表,作为搜索 songAttribute_df 的准则。 在你看下面的代码之前,请允许我提请你注意一个简单的问题。 由于这段代码很长,为了帮助你解读它,我们做了注释。可以用以下方法创建Python行注释 现在,花些时间使用前面的图和下面截图中的代码,了解songIntegrate_df和songAttribute_df之间的联系是如何建立的。

adding_columns = ['Acousticness','Danceability','Duration','Energy','Explicit','Instrumentalness',
                  'Liveness','Loudness','Mode','Speechiness','Tempo','TimeSignature''Valence']
template = 'Index= {} - The song {} by {} was integrated using sitution {}.'
for i, row in songIntegrate_df.iterrows():
    filled = False
    Artists = row.Artists.split(',')
    Artists = list(map(str.strip,Artists))
    # Situation 1
    BM = songAttribute_df.Name == row.Name
    if(sum(BM) == 1):
        for col in adding_columns:
            songIntegrate_df.loc[i,col]= songAttribute_df[BM][col].values[0]
        filled = True
        print(template.format(i,row.Name,row.Artists,1))
    # Situation 2
    elif(sum(BM) > 1):
        wdf = songAttribute_df[BM]
        if(len(Artists)==1):
            BM2 = wdf.Artist.str.contains(Artists[0])
            if(sum(BM2)==1):
                for col in adding_columns:
                    songIntegrate_df.loc[i,col]= wdf[BM2][col].values[0]
                filled = True
                print(template.format(i,row.Name,row.Artists,2))
    # Situation 3
    if((not filled) and len(Artists)>1):
        BM2= (songAttribute_df.Name.str.contains(row.Name)&songAttribute_df.Artist.isin(Artists))
        if(sum(BM2)==1):
            for col in adding_columns:
                songIntegrate_df.loc[i,col]= songAttribute_df[BM2][col].values[0]
            filled = True
            print(template.format(i,row.Name,row.Artists,3))
    if(not filled):
        # Situation 4
        BM2 = songAttribute_df.Name.str.contains(row.Name)
        if(sum(BM2)==1):
            for artist in Artists:
                if(artist == songAttribute_df[BM2].Artist.iloc[0]):
                    for col in adding_columns:
                        songIntegrate_df.loc[i,col]= songAttribute_df[BM2][col].values[0]
                    filled = True
                    print(template.format(i,row.Name,row.Artists,4))        
        # Situation 5
        if(sum(BM2)>1):
            wdf2 = songAttribute_df[BM2]
            BM3 = wdf2.Artist.isin(Artists)
            if(sum(BM3)>0):
                wdf3 = wdf2[BM3]
                for i3, row3 in wdf3.iterrows():
                    if(row3.Name == row.Name):
                        for col in adding_columns:
                            songIntegrate_df.loc[i,col]= row3[col]
                        filled = True
                        print(template.format(i,row.Name,row.Artists,5))
image.png
songIntegrate_df
image.png

在成功运行前面的代码后(可能需要一段时间),花些时间研究它提供的报告。如果你注意了,那么你会知道,每次发现歌曲之间的连接时,代码都会被打印出来。如果连接是可能的,这也会发生。研究打印出来的报告,看看这些情况的频率。 回答以下问题。

  1. 1. 哪种情况是最频繁的?

  2. 2. 哪种情况出现的频率最低?

  3. 3. songIntegrate_df中的所有行是否都填写了songAttribute_df中的值?

最后一个问题的答案是否定的--运行 songIntegrate_df.info() 只会显示 7,213 行中的 4,045 行是由 songAttribute_df 填写的。关于这个数据没有被完全填充的一个关键问题是,看看在顶级歌曲和非顶级歌曲之间是否存在有意义的区分。如果有任何有意义的区分,那么songAttribute_df中列出的数值就变得不那么有价值了。这是因为我们的目标是研究歌曲属性对歌曲成为顶级歌曲的影响。所以,在进入下一个填充之前,我们先研究一下这个问题。 下面的截图显示了songIntegrate_df.Top_song这两个二元变量的或然率表,以及缺失值。它还显示了关联的卡方检验的P值。

songIntegrate_df.info()
image.png
B_MV = songIntegrate_df.Acousticness.isna()
B_MV.rename('Missing Values',inplace=True)
contigency_table = pd.crosstab(songIntegrate_df.Top_song,B_MV)
contigency_table
image.png
from scipy.stats import chi2_contingency
p_value = chi2_contingency(contigency_table)[1]
p_value #0.07952275342130063

在研究了前面的截图后,我们可以得出结论,没有足够的证据让我们拒绝这样的假设:缺失的值与歌曲是否为顶级歌曲没有关系。 这使我们的工作更容易,因为我们不需要做任何事情,只需要在开始使用artist_df来填充songIntegrate_df之前删除不包含数值的行。下面的代码使用.drop()函数来删除songIntegrate_df中songAttribute_df未能填充的行。注意,B_MV变量来自前面截图中的代码。

dropping_index = songIntegrate_df[B_MV].index
songIntegrate_df.drop(index = dropping_index,inplace=True)

成功运行前面的代码,在进入下一步之前,即使用artist_df来填充songIntegrate_df的其余部分,运行songIntegrate_df.info()来评估DataFrame的状态,并确保下降按计划进行。

songIntegrate_df.info()
image.png

5.5从artist_df中填充songIntegrate_df

songIntegrate_df的最后六个属性,即Artists_n_followers、n_male_artsits、n_female_artsits、n_bands、artist_average_years_ after_first_albums和artist_average_number_albums,将由artist_df填充。我们在这里面临的实体识别挑战比整合SongIntegrate_df和SongAttribute_df时要简单得多。 artist_df中的数据对象的定义是艺术家,而这只是songIntegrate_df中数据对象定义的一个部分。我们需要做的就是在artist_df中找到songIntegrate_df每首歌的唯一艺术家或艺术家,然后填入songIntegrate_df。 我们需要在这里填写的所有属性都需要来自artist_df的信息,但不会有直接的填充。所有上述的属性都需要用artist_df的信息来计算。 在数据整合之前,我们需要对artist_df执行一项预处理任务。我们需要使artist_df可以通过艺术家的名字进行搜索;也就是说,我们必须将artist_df的索引设置为艺术家。下面这行代码确保了这一点。下面的代码还删除了X列,在这一点上它没有任何作用。

artist_df = artist_df.set_index('Artist').drop(columns=['X'])

现在,在进入数据整合部分之前,给可搜索的artist_df一个机会,让它向你展示如何轻松收集每个艺术家的信息。例如,试试artist_df.loc['Drake']或任何其他你可能知道的艺术家。

artist_df.loc['Drake']
image.png
artist_df.index.values
image.png

以下截图中的代码在songIntegrate_df的所有行中循环,以找到所需的歌曲艺术家的信息,并填入songIntegrate_df的最后六个属性。在每一次迭代中,代码都会将songIntegrate_df中的歌曲的艺术家分开,并检查artists_df是否包含所有歌曲艺术家的信息。如果没有,代码就会终止,因为artist_df中没有足够的信息来填写这六个属性。如果这些信息存在,代码将所有六个新的属性赋值为零,然后在一些条件和逻辑计算中,更新这些零值。 在你钻研这段相当大的代码之前,有几句话要提醒你。首先,这些代码的行数相当长,所以它们可能被切成了不止一行。有两种不同的方法可以将一行代码切割成更多行。较好的方法叫做隐式续行;每当在小括号,(,大括号,{,或方括号,[之后断行时,Python 假定还有更多的内容,并在寻找时自动转到下一行。另一种方法--如果可以的话,我们尽量避免--被称为显式续行,Python 不会在下一行寻找更多的内容,除非我们在行尾使用反斜杠(\)明确要求这样做。 第二点需要注意的是,代码中使用了所谓的增强型算术赋值,以节省编写代码时的空间。这些类型的赋值是用来避免在计算变量的新值时涉及到变量的旧值时,将同一个变量写两次。例如,你可以写x+=1而不是x=x+1,或者你可以写y/=5而不是y=y/5。在以下代码中,有多处使用了增强的算术赋值。

for i,row in songIntegrate_df.iterrows():
    Artists = row.Artists.split(',')
    Artists = list(map(str.strip,Artists))
    ArtistsIn_artist_df = True
    for artist in Artists:
        if(artist not in artist_df.index.values):
            ArtistsIn_artist_df= False
            break
    if(not ArtistsIn_artist_df):
        continue

    songIntegrate_df.loc[i,'Artists_n_followers'] = 0
    songIntegrate_df.loc[i,'n_male_artists'] = 0
    songIntegrate_df.loc[i,'n_female_artists'] = 0
    songIntegrate_df.loc[i, 'artist_average_years_after_first_album'] = 0
    songIntegrate_df.loc[i, 'artist_average_number_albums'] = 0
    songIntegrate_df.loc[i,'n_bands'] = 0
        
    for artist in Artists:
        songIntegrate_df.loc[i,'Artists_n_followers'] += artist_df.loc[artist].Followers
        if(artist_df.loc[artist]['Group.Solo']=='Solo'):
            if(artist_df.loc[artist].Gender == 'M'):            
                songIntegrate_df.loc[i,'n_male_artists'] += 1
            if(artist_df.loc[artist].Gender == 'F'):
                songIntegrate_df.loc[i,'n_female_artists'] += 1

        if(artist_df.loc[artist]['Group.Solo']=='Group'):
            if(artist_df.loc[artist].Gender == 'M'):
                songIntegrate_df.loc[i,'n_male_artists'] += 2
            if(artist_df.loc[artist].Gender == 'F'):
                songIntegrate_df.loc[i,'n_female_artists'] += 2
            songIntegrate_df.loc[i,'n_bands'] += 1
        First_date_on_Billboard = int(row.First_date_on_Billboard[:4])
        songIntegrate_df.loc[i, 'artist_average_years_after_first_album'] += \
            (First_date_on_Billboard - int(artist_df.loc[artist].YearFirstAlbum))

        songIntegrate_df.loc[i,
            'artist_average_number_albums'] += int(artist_df.loc[artist].NumAlbums)
    
    songIntegrate_df.loc[i,'artist_average_years_after_first_album'] /= len(Artists)
    songIntegrate_df.loc[i, 'artist_average_number_albums'] /= len(Artists)

你可能已经注意到,当歌曲的艺术家是一个团体,并且性别被列为男性时,代码会在n_male_artists中加入2,而当歌曲的艺术家是一个团体,并且性别被列为女性时,代码会在n_female_artists中加入2。这包括一个假设,即所有的团体只有两个艺术家。由于我们没有其他的来源,所以我们可以更准确地了解这些情况,这是一个合理的假设,让我们继续下去,同时避免在数据中施加过多的偏见。然而,如果要将结果呈现给任何感兴趣的决策者,就必须告知这一假设。 在成功运行前面的代码后,运行 songIntegrate_df.info() 来调查 songIntegrate_df 中有多少行是用 artist_df 的信息完成的。你会看到,在4,045首歌曲中,有3,672首已经完成。

songIntegrate_df.info()
image.png

虽然这是songIntegrate_df中的主要部分,但我们仍然需要确保没有由于与歌曲是否为顶级歌曲有关的原因而出现的缺失值。 因此,我们将做一个与图12.16类似的分析。下面的截图显示了在更新了 songIntegrate_df 的情况下进行同样分析的结果。

B_MV = songIntegrate_df.Artists_n_followers.isna()
B_MV.rename('Missing Values',inplace=True)
contigency_table = pd.crosstab(songIntegrate_df.Top_song,B_MV)
contigency_table
image.png
from scipy.stats import chi2_contingency
p_value = chi2_contingency(contigency_table)[1]
p_value # 0.4931640410927335 (P值大于原先定的显著性水平,所以接受原假设,即没有关系。)

在研究了前面的截图之后,我们可以看到,没有任何有意义的模式可以指出一首歌曲成为顶级歌曲和它在这个时刻有缺失值的倾向之间可能存在的联系。因此,我们可以放心地删除有缺失值的行,然后继续。下面这行代码就完成了规定的删除。

droping_indices = songIntegrate_df[B_MV].index.values
songIntegrate_df.drop(index = droping_indices, inplace=True)

前面的代码使用.drop()函数来删除songIntegrate_ df中artist_df未能填充的行。注意,B_MV变量来自前面的截图中的代码。 恭喜你--你已经整合了这三个数据源 这要归功于你对数据结构的出色理解,以及你看到这些数据源中每个数据对象的定义的能力。此外,你能够设想出一个数据集,可以容纳所有来源的信息,同时对你的分析目标有用。 在我们进行分析之前,我们需要解决另一个挑战。每当我们把不同来源的数据汇集在一起时,我们可能无意中创造了一种情况,我们在前面称之为数据冗余(挑战6--数据冗余)。正如我们之前提到的,数据冗余是指你重复相同的属性,但你重复相同的信息。

5.6 检查数据的冗余度

正如我们之前提到的,这一部分涉及到挑战6--数据冗余。尽管我们在本书中从未处理过这个挑战,但我们已经看到了许多调查属性之间关系的例子。如果songIntegrate_df中的一些属性相互之间有很强的关系,那就可以成为我们的红旗,表明数据冗余。就这么简单! 所以,让我们开始吧。首先,我们将使用相关分析来研究数字属性之间的关系。然后,我们将使用箱形图和t检验来调查数字属性和分类属性之间的关系。 如果我们不是只有一个分类属性,我们也会调查分类属性之间的关系。如果你确实有一个以上的分类属性,为了评估数据的冗余度,你就需要使用或然率表和独立度的卡方检验。

5.6.1 检查数字属性中的数据冗余度

正如我们之前提到的,为了评估数据冗余的存在,我们将使用相关分析。如果两个属性之间的相关系数很高(我们将使用0.7的经验法则),那么这意味着两个属性所呈现的信息过于相似,可能存在数据冗余的情况。 下面的代码使用.corr()函数来计算num_atts中明确列出的数字属性之间的相关性。该代码还使用了一个布尔掩码(BM)来帮助我们的眼睛找到大于0.7或小于-0.7的相关系数。请注意代码中必须包括.astype(float)的原因:在数据整合过程中,一些属性可能被转为字符串而不是数字。

num_atts = ['Acousticness''Danceability''Duration''Energy',
       'Instrumentalness''Liveness''Loudness''Mode''Speechiness',
       'Tempo''TimeSignature''Valence''Artists_n_followers',
       'n_male_artists''n_female_artists''n_bands',
       'artist_average_years_after_first_album',
       'artist_average_number_albums']
corr_Table = songIntegrate_df[num_atts].astype(float).corr()
BM = (corr_Table > 0.7) | (corr_Table<-0.7)
corr_Table[BM]
image.png

在成功运行前面的代码后,将出现一个相关矩阵,其中大部分单元格为NaN,但只有那些相关系数大于0.7或小于-0.7的单元格。你会注意到,唯一被标记的相关系数是在能量和响度之间。 这两个属性之间有关系是有道理的。由于这些属性来自同一来源,我们将把我们的信心放在这些属性的创造者身上,他们确实显示了不同的价值,并且大约30%的两者之间的信息是值得保留的。 在此,我们可以得出结论,在数字属性之间不存在数据冗余的问题。接下来,我们将调查分类属性和数字属性之间的关系是否太强。

5.6.2 检查数字和分类属性之间的数据冗余情况

为了评估是否存在数据冗余,与我们对数字属性所做的类似,我们需要检查属性之间的关系。由于这些属性具有不同的性质--即数字和分类--我们需要使用boxplots和t检验。 在这一点上,唯一被整合并具有分析价值的分类属性是Explicit属性。为什么不是top_song属性?顶级歌曲对我们来说确实有分析价值--事实上,它是我们分析的铰链--但它不是从不同的来源整合而来的。相反,是为我们的分析而计算的。一旦我们进入这个例子的分析部分,我们将看看这个属性和所有其他属性之间的关系。为什么不是名字或艺术家?这些仅仅是识别列。为什么不是First_date_on_Billboard?这是一个临时性的属性,允许我们在需要来自多个数据源的信息时进行计算。这个属性将在分析之前被放弃。 下面的代码创建了箱形图,显示了数字属性和分类属性之间的关系;也就是Explicit。此外,该代码使用scipy.stats中的ttest_ind()函数来运行t检验。

from scipy.stats import ttest_ind

for n_att in num_atts:
    sns.boxplot(data=songIntegrate_df, 
                y=n_att,x='Explicit')
    plt.show()
    
    BM = songIntegrate_df.Explicit == True
    print(ttest_ind(songIntegrate_df[BM][n_att],
                    songIntegrate_df[~BM][n_att]))
    print('-----------------divide-------------------')
image.png

运行前面的代码后,每个数字属性都会出现一个框图和评估明确属性和数字属性之间关系的t检验结果。在研究了输出结果后,你会意识到,除了响度、模式和效力外,明确属性与所有的数字属性都有关系。由于Explicit属性不太可能包含任何尚未包含在数据中的新信息,我们将标记Explicit,以防止可能的数据冗余。 请注意,我们不一定需要在数据预处理阶段删除Explicit。我们如何处理数据冗余将取决于分析目标和工具。例如,如果我们打算使用决策树来查看导致一首歌曲成为顶级歌曲的多元模式,那么我们就不需要对数据冗余做任何处理。这是因为决策树有一个机制来选择有助于算法成功的特征(属性)。另一方面,如果我们使用K-means对歌曲进行分组,那么我们就需要删除Explicit,因为该信息已经被引入到其他属性中。如果我们包括它两次,那么它将在我们的结果中产生偏差。

5.7分析报告

最后,数据源被适当地整合到SongIntegrate_df中,数据集就可以进行分析了。我们的目标是回答是什么使一首歌曲成为顶级歌曲的问题。既然数据已经被预处理,我们可以采用不止一种方法来回答这个问题。在这里,我们将使用其中的两种。我们将使用数据可视化来识别顶级歌曲的单变量模式,我们将使用决策树来提取多变量模式。 我们将从数据可视化过程开始。 在我们开始之前,由于该属性被标记为冗余,因此没有必要为上述任何分析工具删除Explicit属性。正如我们之前提到的,决策树有一个智能的特征选择机制,所以对于数据可视化来说,保留Explicit只意味着多了一个简单的可视化,不会干扰到其他的可视化。

5.7.1 在顶级歌曲中寻找模式的数据可视化方法

为了研究是什么使一首歌曲成为顶级歌曲,我们可以研究songIntegrate_df中所有其他属性与Top_song属性的关系,看看是否出现了任何有意义的模式。 下面的代码为songIntegrate_df中的每个数字属性创建了一个箱形图,以调查数字属性的值是否在两个人群中发生变化:顶级歌曲和非顶级歌曲。该代码还输出了一个t检验的结果,从统计学上回答了同一个问题。此外,代码还输出了两个种群的中位数,以防难以识别箱形图中两个种群的数值之间的微小比较。

from scipy.stats import ttest_ind

for n_att in num_atts:
    sns.boxplot(data=songIntegrate_df, 
                y=n_att,x='Top_song')
    plt.show()
    
    BM = songIntegrate_df.Top_song == True
    print(ttest_ind(songIntegrate_df[BM][n_att],
                    songIntegrate_df[~BM][n_att]))
    dic = {'not Top Song Median'
           songIntegrate_df[~BM][n_att].median(),
           'Top Song Median'
           songIntegrate_df[BM][n_att].median()}
    print(dic)
    
    print('-----------------divide-------------------')
image.png

此外,下面的代码输出一个或然率表,显示两个分类属性之间的关系;即songIntegrate_df.Top_song和songIntegrate_df.Explicit。它还打印出了这两个分类属性的独立的卡方检验的P值。

from scipy.stats import chi2_contingency

contingency_table = pd.crosstab(
    songIntegrate_df.Top_song, songIntegrate_df.Explicit)

print(contingency_table)
print('p-value = {}'.format(
    chi2_contingency(contingency_table)[1]))
image.png

在研究了前面两段代码的输出后,我们可以得出以下结论。

  • • 没有证据可以拒绝无效假设,即Top_song属性与Duration、Energy、Instrumentalness、Liveness、Loudness、Mode、Speechiness、Explicit和TimeSignature属性没有关系。这意味着,通过查看这些属性的值,无法预测顶级歌曲的情况。

  • • 顶级歌曲在Acousticness、Tempo、n_male_artists、n_bands、artist_average_years_after_first_ album和artist_average_number_albums等属性上的数值往往较小。

  • • 在可舞性、有效性、艺术家_n_followers、n_female_artists属性中具有较大数值的歌曲,往往更容易成为顶级歌曲。 当然,这些模式听起来太笼统了,它们应该是;这是因为它们是单变量的。接下来,我们将应用决策树来找出多变量模式,这可能有助于我们理解一首歌曲如何成为顶级歌曲。

5.7.2 在顶级歌曲中寻找多变量模式的决策树方法

正如我们在第七章 "分类 "中所讨论的,决策树因其透明性和能够从数据中呈现有用的多变量模式而闻名。在这里,我们想用决策树算法来看看导致一首歌上升到广告牌前十名的模式。 下面的代码使用sklearn.tree的DecisionTreeClassifier来创建一个分类模型,目的是找到独立属性和依赖属性之间的关系;也就是Top_song。一旦模型使用这些数据进行了训练,代码将使用graphviz来可视化训练好的决策树。在代码的最后,提取的图将被保存在一个名为TopSongDT.pdf的文件中。成功运行这段代码后,你应该能够在你拥有Jupyter笔记本文件的同一文件夹中找到该文件。

注意! 如果你以前从未在你的电脑上使用过graphviz,你可能必须先安装它。

要安装graphviz,你所需要做的就是运行下面这段代码。在成功运行一次这段代码后,graphviz将被永久地安装在你的电脑上。

pip install graphviz

在运行下面的代码之前,请注意下面代码中使用的决策树模型已经被调整过了。在第7章 "分类 "中,我们提到调整决策树是非常重要的。然而,我们在本书中并没有介绍如何做。超参数及其调整值是 criterion= 'entropy', max_depth= 10, min_samples_split= 30, and splitter= 'best'。

from sklearn.tree import DecisionTreeClassifier, export_graphviz
import graphviz

y = songIntegrate_df.Top_song.replace(
    {True:'Top Song',False:'Not Top Song'})
Xs = songIntegrate_df.drop(columns = [
    'Name','Artists','Top_song','First_date_on_Billboard'])

classTree = DecisionTreeClassifier(criterion= 'entropy'
                                   max_depth= 10,
                                   min_samples_split= 30,
                                   splitter= 'best')
classTree.fit(Xs, y)

dot_data = export_graphviz(classTree, 
                           out_file=None
                           feature_names=Xs.columns,  
                           class_names=['Not Top Song',
                                        'Top Song'], 
                           filled=True,
                           rounded=True
                           special_characters=True)  
graph = graphviz.Source(dot_data) 
graph.render(filename='TopSongDT')

成功运行前面的代码后,TopSongDT.pdf文件将被保存在你的电脑上,其中包含可视化的决策树。这个树显示在下图中。在这个例子中,这个图没有与你分享,以便你可以研究它;你可以看到,决策树相当大,而我们的空间非常小。 然而,你可以看到,有很多有意义的多变量模式形成的数据,这可以帮助我们预测顶级歌曲。

image.png

自己打开TopSongDT.pdf并研究它。例如,你会看到区分顶级歌曲和非顶级歌曲的最重要属性是艺术家_n_followers。再比如,如果歌曲没有拥有高追随者的艺术家,那么这首歌曲成为顶级歌曲的最好机会就是这首歌曲是明确的、可跳舞的,并且来自经验较少的艺术家。在决策树中还有许多像这样的有用模式。继续研究TopSongDT.pdf以找到它们。

5.8 例子总结

在这个例子中,我们执行了许多步骤,以达到songIntegrate_df处于一种能够进行分析并找到有用信息的状态。为了唤起我们的记忆,这些是我们采取的步骤。

  1. 1. 检查所有三个数据源中是否有重复的数据

  2. 2. 设计最终的综合数据集的结构

  3. 3. 分三个步骤整合数据源

  4. 4. 检查数据的冗余度

  5. 5. 进行分析

6 总结

祝贺你在本章中取得的卓越进展。首先,在熟悉六种常见的数据整合挑战之前,我们了解了数据融合和数据整合之间的区别。然后,通过三个全面的例子,我们利用在本书中掌握的编程和分析工具来面对这些数据整合挑战,并对数据源进行预处理,从而达到分析的目的。 在下一章中,我们将重点讨论另一个数据预处理概念,由于计算资源的限制,这个概念非常关键,特别是对于算法数据分析来说:数据还原。 在你开始你的数据还原之旅之前,花点时间,尝试一下下面的练习,以巩固你的学习。

7 练习

在这个练习中,我们将看到一个数据融合的例子。我们在这个练习中要使用的案例已经在本章的数据融合例子中介绍过了,所以在继续这个练习之前,请回去再读一遍。 在这个例子中,我们想把Yield.csv和Treatment.csv整合起来,看看水量是否能影响产量。 为实现这一目标,请执行以下操作。 a) 使用pd.read_csv()将Yield.csv读入yield_df, 并将Treatment.csv读入treatment_df。

import pandas as pd 
yield_df = pd.read_csv('https://raw.githubusercontent.com/PacktPublishing/Hands-On-Data-Preprocessing-in-Python/main/Chapter12/Yeild.csv')
yield_df.head()
image.png
treatment_df = pd.read_csv('https://raw.githubusercontent.com/PacktPublishing/Hands-On-Data-Preprocessing-in-Python/main/Chapter12/Treatment.csv')
treatment_df.head()
image.png

b) 将treatment_df中的点绘制成散点图。使用颜色的维度来增加每个点的配水量。

import matplotlib.pyplot as plt 
plt.figure(figsize=(10,8))
treatment_df.plot.scatter('longitude','latitude',c='water')
image.png

c) 画出产量_df中各点的散点图。使用颜色的维度来增加每个点的收获量。

yield_df.plot.scatter('longitude','latitude',c='harvest')
image.png

d) 创建一个散点图,结合步骤b和c中的视觉。

plt.figure(figsize=(20,8))
plt.subplot(1,2,1)
plt.scatter(treatment_df.longitude,treatment_df.latitude,c=treatment_df.water)
plt.subplot(1,2,2)
plt.scatter(yield_df.longitude,yield_df.latitude,c=yield_df.harvest)
plt.colorbar()
plt.tight_layout()
image.png

e) 从前面步骤的散点图中,我们可以推断出水站之间是等距离的。基于这一认识,计算各水点之间的距离,并称之为半径。我们将在下面的一组计算中使用这个变量。 f) 首先,使用下面的代码来创建calculateDistance()函数。

import math
def calculateDistance(x1,y1,x2,y2):
    dist = math.sqrt((x2 - x1)**2 + (y2 - y1)**2)
    return dist

然后,使用下面的代码和前面我们刚创建的函数,创建waterRecieved()函数,这样我们就可以把它应用于treatment_df的行的函数。

def WaterReceived(r):
    w = 0
    for i, rr in treatment_df.iterrows():
        distance = calculateDistance(rr.longitude,
                                     rr.latitude,
                                     r.longitude,
                                     r.latitude) 
        if (distance< radius):
            w= w + rr.water * ((radius-distance)/radius)
    return w

g) 对yield_df的行应用waterRecieved(),并将每行的新计算值添加到water的列中。

h) 研究新更新的yield_df。你刚刚融合了这两个数据源。回去研究一下这些步骤,特别是创建waterRecieved()函数的过程。 i) 画一个yield_df.harvest和yield_df.water的散点图。 我们是否看到yield_df.water对yield_df.harvest有任何影响? j) 使用相关系数来证实你在前一步的观察。


【声明】内容源于网络
0
0
谁说菜鸟不会数据分析
以大数据分析为驱动,spss/R/python/数据分析交流技术分享,实用教程干货,敬请期待,B站UP主:谁说菜鸟不会数据分析 有更多在线实操视频。
内容 498
粉丝 0
谁说菜鸟不会数据分析 以大数据分析为驱动,spss/R/python/数据分析交流技术分享,实用教程干货,敬请期待,B站UP主:谁说菜鸟不会数据分析 有更多在线实操视频。
总阅读2
粉丝0
内容498