数据处理与清洗
当前的 dataframe 不是我们想要的格式。为清理它,您需要在逗号位置将列 "0" 拆分为多列。可使用 str.split() 方法完成。
df1 = df[0].str.split(',', expand=True)
df1.head(10)

这样看起来好多了,但仍有工作要做。dataframe 的每行两端都带有方括号。您可以使用 strip() 方法移除列 "0" 上的左方括号。
df1[0] = df1[0].str.strip('[')
df1.head(10)

该表缺少表头。您可以使用 find_all() 方法获取表头。
col_labels = soup.find_all('th')
与表格行类似,您可以用 BeautifulSoup 提取表头中 html 标签之间的文本。
all_header = []
col_str = str(col_labels)
cleantext2 = BeautifulSoup(col_str, "lxml").get_text()
all_header.append(cleantext2)
print(all_header)
['[Place, Bib, Name, Gender, City, State, Chip Time, Chip Pace, Gender Place, Age Group, Age Group Place, Time to Start, Gun Time, Team]']
然后,您可以将表头列表转换为一个 pandas dataframe。
df2 = pd.DataFrame(all_header)
df2.head()
| 0 | |
|---|---|
| 0 | [Place, Bib, Name, Gender, City, State, Chip T... |
类似地,您可以在逗号处将所有行的列 "0" 拆分成多列。
df3 = df2[0].str.split(',', expand=True)
df3.head()

可以使用 concat() 方法将两个 dataframe 合并为一个,如下所示。
frames = [df3, df1]
df4 = pd.concat(frames)
df4.head(10)

如下所示,将第一行设为表头。
df5 = df4.rename(columns=df4.iloc[0])
df5.head()

此时,表格已几乎整理完毕。为便于分析,您可以先获取数据概览,如下所示。
df5.info()
df5.shape
<class 'pandas.core.frame.DataFrame'>
Int64Index: 597 entries, 0 to 595
Data columns (total 14 columns):
[Place 597 non-null object
Bib 596 non-null object
Name 593 non-null object
Gender 593 non-null object
City 593 non-null object
State 593 non-null object
Chip Time 593 non-null object
Chip Pace 578 non-null object
Gender Place 578 non-null object
Age Group 578 non-null object
Age Group Place 578 non-null object
Time to Start 578 non-null object
Gun Time 578 non-null object
Team] 578 non-null object
dtypes: object(14)
memory usage: 70.0+ KB
(597, 14)
该表共有 597 行、14 列。您可以删除所有包含缺失值的行。
df6 = df5.dropna(axis=0, how='any')
另外请注意,df5 的第一行重复了表头。可通过以下代码删除。
df7 = df6.drop(df6.index[0])
df7.head()

您可以进一步清洗数据,重命名 '[Place' 和 ' Team]' 列。Python 对空格非常敏感,请确保在 ' Team]' 的引号后包含空格。
df7.rename(columns={'[Place': 'Place'},inplace=True)
df7.rename(columns={' Team]': 'Team'},inplace=True)
df7.head()

最后一步的清洗是移除 ' Team' 列中单元格的右方括号。
df7['Team'] = df7['Team'].str.strip(']')
df7.head()

虽然花了一些时间,但此时 dataframe 已达到期望格式。现在可以进入更有趣的部分,开始绘图并计算一些有意思的统计量。
数据分析与可视化
第一个问题是:跑者的平均完赛时间(分钟)是多少?您需要将 "Chip Time" 列转换为分钟。一个方法是先将该列转换为列表以便处理。
time_list = df7[' Chip Time'].tolist()
# You can use a for loop to convert 'Chip Time' to minutes
time_mins = []
for i in time_list:
h, m, s = i.split(':')
math = (int(h) * 3600 + int(m) * 60 + int(s))/60
time_mins.append(math)
#print(time_mins)
下一步是将该列表放回 dataframe,并新建一列(Runner_mins),以分钟表示跑者的芯片计时。
df7['Runner_mins'] = time_mins
df7.head()

下面的代码展示了如何仅针对 dataframe 中的数值列计算统计量。
df7.describe(include=[np.number])
| Runner_mins | |
|---|---|
| count | 577.000000 |
| mean | 60.035933 |
| std | 11.970623 |
| min | 36.350000 |
| 25% | 51.000000 |
| 50% | 59.016667 |
| 75% | 67.266667 |
| max | 101.300000 |
有趣的是,所有跑者的平均芯片计时约为 60 分钟。最快的 10 公里跑者在 36.35 分钟内完赛,最慢的为 101.30 分钟。
箱线图是另一种用于可视化汇总统计(最大值、最小值、中位数、第一四分位、第三四分位及离群值)的有用工具。下面用箱线图展示跑者的统计信息。为便于可视化,建议先从 matplotlib 自带的 pylab 模块导入参数,并设置统一的图形尺寸,以免每张图都单独设置。
from pylab import rcParams
rcParams['figure.figsize'] = 15, 5
df7.boxplot(column='Runner_mins')
plt.grid(True, axis='y')
plt.ylabel('Chip Time')
plt.xticks([1], ['Runners'])
([<matplotlib.axis.XTick at 0x570dd106d8>],
<a list of 1 Text xticklabel objects>)

第二个问题是:跑者的完赛时间是否符合正态分布?
下面是使用 seaborn 库绘制的跑者芯片计时分布图。该分布看起来几乎接近正态。
x = df7['Runner_mins']
ax = sns.distplot(x, hist=True, kde=True, rug=False, color='m', bins=25, hist_kws={'edgecolor':'black'})
plt.show()

第三个问题关注不同年龄组的男女是否存在表现差异。下面是男女芯片计时的分布图。
f_fuko = df7.loc[df7[' Gender']==' F']['Runner_mins']
m_fuko = df7.loc[df7[' Gender']==' M']['Runner_mins']
sns.distplot(f_fuko, hist=True, kde=True, rug=False, hist_kws={'edgecolor':'black'}, label='Female')
sns.distplot(m_fuko, hist=False, kde=True, rug=False, hist_kws={'edgecolor':'black'}, label='Male')
plt.legend()
<matplotlib.legend.Legend at 0x570e301fd0>

从分布上看,女性平均比男性更慢。您可以使用 groupby() 方法分别计算男女的汇总统计,如下所示。
g_stats = df7.groupby(" Gender", as_index=True).describe()
print(g_stats)
Runner_mins \
count mean std min 25% 50%
Gender
F 163.0 66.119223 12.184440 43.766667 58.758333 64.616667
M 414.0 57.640821 11.011857 36.350000 49.395833 55.791667
75% max
Gender
F 72.058333 101.300000
M 64.804167 98.516667
所有女性与男性的平均芯片计时分别约为 66 分钟与 58 分钟。下面给出男女完赛时间的并列箱线图对比。
df7.boxplot(column='Runner_mins', by=' Gender')
plt.ylabel('Chip Time')
plt.suptitle("")
C:\Users\smasango\AppData\Local\Continuum\anaconda3\lib\site-packages\numpy\core\fromnumeric.py:57: FutureWarning: reshape is deprecated and will raise in a subsequent release. Please use .values.reshape(...) instead
return getattr(obj, method)(*args, **kwds)
Text(0.5,0.98,'')

结论
在本教程中,您使用 Python 进行了网页抓取。您使用 Beautiful Soup 库解析 html 数据并将其转换为可用于分析的形式。您在 Python 中对数据进行了清洗,并使用 Python 的 matplotlib 和 seaborn 库创建了有用的图表(箱线图、条形图与分布图)来揭示有趣的趋势。完成本教程后,您应该能够轻松使用 Python 从网页抓取数据,应用清洗技术,并从数据中提炼有价值的洞见。
如果您想进一步学习 Python,请参加 DataCamp 的免费课程 Intro to Python for Data Science,并查看我们的 如何使用 python 抓取亚马逊的教程。
网页抓取(Web scraping)是指使用程序或算法从网络中提取并处理大量数据的过程。无论您是数据科学家、工程师,还是需要分析大规模数据集的从业者,具备从网页抓取数据的能力都非常有用。举个例子,您在网上找到了数据,但没有直接的下载方式,此时就可以使用 Python 进行网页抓取,把数据提取成便于导入的可用格式。
在本教程中,您将学习以下内容:
本教程所用数据集来自 2017 年 6 月在俄勒冈州希尔斯伯勒举行的一场 10 公里赛。您将分析 10 公里跑者的表现,并回答以下问题:
使用 Beautiful Soup 进行网页抓取
在 Jupyter Notebook 中,首先应导入必要的模块(pandas、numpy、
matplotlib.pyplot、Seaborn)。如果您尚未安装 Jupyter Notebook,建议通过 Anaconda Python 发行版进行安装。为便于直接显示图表,请确保包含如下所示的%matplotlibinline 语句。要进行网页抓取,还需要导入下列库。
urllib.request模块用于打开 URL。BeautifulSoup 包用于从 html 文件中提取数据。BeautifulSoup 库的名称为bs4,表示 BeautifulSoup 第 4 版。在导入必要模块后,指定包含数据集的 URL,并将其传递给
urlopen()以获取页面的 html。获取页面的 html 只是第一步。接下来需要从 html 创建一个
BeautifulSoup对象。方法是将 html 传入BeautifulSoup()函数。BeautifulSoup 包用于解析 html,即将原始 html 文本拆解为 Python 对象。第二个参数lxml是 html 解析器,暂时无需关心其细节。soup对象可以让您提取被抓取网站的关键信息,例如如下所示获取页面标题。您也可以获取网页的纯文本,并快速打印出来,检查是否符合预期。
您可以通过在网页任意位置右键并选择 "Inspect" 来查看网页的 html。结果大致如下图所示。
可以使用 soup 的
find_all()方法提取网页中的有用 html 标签。常用的标签包括 < a >(超链接)、< table >(表格)、< tr >(表格行)、< th >(表头)和 < td >(单元格)。下面的代码演示如何提取网页中的所有超链接。如上所示,html 标签有时会带有 class、src 等属性,这些属性为元素提供了附加信息。您可以用 for 循环和
get("href")方法,仅提取并打印出超链接。若只打印表格行,请在
soup.find_all()中传入tr参数。本教程的目标是将网页中的一个表格转换为 dataframe,以便用 Python 更容易地进行处理。为此,先以列表形式获取所有表格行,然后将该列表转换为 dataframe。下面的 for 循环遍历表格行,并打印各行的单元格。
上面的输出显示每一行都带有嵌入的 html 标签,这并不是我们想要的结果。您可以使用 Beautiful Soup 或正则表达式去除 html 标签。
最简单的去除 html 标签的方法是使用 BeautifulSoup,只需一行代码。将目标字符串传入
BeautifulSoup(),再使用get_text()方法提取不含 html 标签的文本。不建议使用正则表达式,因为代码更长且容易出错。它需要导入
re(正则表达式)模块。下面的代码展示了如何构建一个正则表达式来匹配 < td > html 标签中的所有字符,并为每一行替换为空字符串。首先,将要匹配的字符串传给re.compile()编译为正则表达式。点、星号和问号(.*?) 会匹配一个左尖括号,后跟任意内容,再跟一个右尖括号。它以非贪婪方式匹配,即匹配尽可能短的字符串。如果省略问号,它将匹配从第一个左尖括号到最后一个右尖括号之间的所有文本。编译正则后,可用re.sub()方法查找所有匹配子串并替换为空字符串。以下完整代码会生成一个空列表,提取每行 html 标签之间的文本,并将其追加到指定列表中。下一步是将列表转换为 dataframe,并使用 Pandas 快速查看前 10 行。