趋近智
好的,让我们将本章所学的技术付诸实践。理论有价值,但将这些技能应用于真实场景可以巩固你的理解。在此动手操作部分,我们将演练一个典型的数据规整流程,模拟从不同来源收集数据、清理数据、进行转换,并将其整合为一个可供分析的数据集。
假设我们正在处理与城市公园使用情况相关的数据。我们有来自问卷调查的公园访客信息(模拟数据库导出)、关于公园内特定活动的事件数据(模拟API响应),以及有关公园自身的一些基本信息(可能从城市网站抓取或以简单文件形式提供)。我们的目标是将这些不同来源的数据整合成一个统一的数据集,以便后续分析,例如,了解哪些活动吸引更多访客,或者识别公园使用中的人口统计规律。
首先,请确保你已安装必要的Python库并将其导入。我们主要会使用pandas进行数据操作,requests(或模拟文件加载)用于API交互,以及可能使用BeautifulSoup等库进行抓取(或模拟文件加载),sqlalchemy进行数据库交互(或通过pandas读取CSV/SQLite模拟)。
import pandas as pd
import numpy as np
import json # 如果从JSON文件加载
# import requests # 如果与真实API交互
# from bs4 import BeautifulSoup # 如果进行实时网页抓取
# from sqlalchemy import create_engine # 如果连接到SQL数据库
# 在本示例中,我们假设数据是从本地文件加载的
# 模拟获取步骤的输出。
我们将从载入模拟数据集开始。假设我们有以下数据:
visitors.csv: 包含访客人口统计信息和访问频率的问卷调查响应。events.json: 一个JSON文件,详细列出了公园中举办的活动,包括公园ID、活动类型和估计参与人数。parks.csv: 关于每个公园的基本信息,如名称、ID和面积(英亩)。# 载入访客调查数据(模拟数据库查询结果)
try:
visitors_df = pd.read_csv('visitors.csv')
print("访客数据载入成功。")
# 显示前几行和信息
print(visitors_df.head())
print(visitors_df.info())
except FileNotFoundError:
print("错误:visitors.csv未找到。请确保文件在正确的目录下。")
# 如果未找到文件,则创建虚拟数据,以便后续代码运行
visitors_df = pd.DataFrame({
'visitor_id': range(1, 101),
'age': np.random.randint(18, 75, 100),
'gender': np.random.choice(['M', 'F', 'NB', None], 100, p=[0.45, 0.45, 0.05, 0.05]),
'visit_frequency': np.random.choice(['Daily', 'Weekly', 'Monthly', 'Rarely', np.nan], 100, p=[0.1, 0.3, 0.4, 0.15, 0.05]),
'primary_park_id': np.random.randint(1, 6, 100)
})
visitors_df.loc[::10, 'age'] = np.nan # 引入一些缺失的年龄数据
# 载入事件数据(模拟API响应)
try:
with open('events.json', 'r') as f:
events_data = json.load(f)
events_df = pd.json_normalize(events_data, record_path='events')
print("\n事件数据载入成功。")
# 显示前几行和信息
print(events_df.head())
print(events_df.info())
except FileNotFoundError:
print("错误:events.json未找到。正在创建虚拟数据。")
events_df = pd.DataFrame({
'event_id': [f'evt{i:03}' for i in range(1, 21)],
'park_id': np.random.randint(1, 6, 20),
'event_type': np.random.choice(['Concert', 'Market', 'Festival', 'Sports'], 20),
'attendance_est': np.random.randint(50, 500, 20),
'event_date': pd.to_datetime(pd.date_range(start='2023-06-01', periods=20, freq='3D'))
})
events_df.loc[[3, 15], 'attendance_est'] = -1 # 引入有问题的数据
# 载入公园详情(模拟抓取或简单文件数据)
try:
parks_df = pd.read_csv('parks.csv')
print("\n公园数据载入成功。")
# 显示前几行和信息
print(parks_df.head())
print(parks_df.info())
except FileNotFoundError:
print("错误:parks.csv未找到。正在创建虚拟数据。")
parks_df = pd.DataFrame({
'Park ID': range(1, 6),
'Park Name': [f'Park {chr(64+i)}' for i in range(1, 6)],
'Size (acres)': [25.5, 102.0, 15.8, 55.1, 220.9],
'Has Playground': [True, True, False, True, True]
})
立即,我们可能会从.info()和.head()的输出中注意到不一致或问题:
primary_park_id 对 park_id 对 Park ID)。event_date可能是一个对象类型)。NaN、None)存在于visitors_df中。events_df中的负数参与人数)。parks_df中的列名包含空格和括号。让我们处理上面发现的问题。
一致的列命名使合并和分析更加容易。我们会将名称转换为小写,并将空格/特殊字符替换为下划线。
# 清理访客列名(大部分已清理)
visitors_df.columns = visitors_df.columns.str.lower().str.replace(' ', '_')
# 清理事件列名
events_df.columns = events_df.columns.str.lower().str.replace(' ', '_')
# 清理公园列名
parks_df.columns = parks_df.columns.str.lower().str.replace(' ', '_').str.replace('[()]', '', regex=True)
# 在合并前,为保持一致性重命名列
parks_df = parks_df.rename(columns={'park_id': 'id', 'park_name': 'name'}) # 如果原始是'Park ID'的示例
visitors_df = visitors_df.rename(columns={'primary_park_id': 'park_id'}) # 对齐列
# 假设events_df的'park_id'已与visitors_df一致
print("\n已清理的列名:")
print("访客:", visitors_df.columns)
print("事件:", events_df.columns)
print("公园:", parks_df.columns)
确保列具有适当的数据类型。例如,event_date应该是一个datetime对象。
# 必要时修正数据类型(示例假设event_date作为对象载入)
if 'event_date' in events_df.columns and events_df['event_date'].dtype == 'object':
events_df['event_date'] = pd.to_datetime(events_df['event_date'], errors='coerce')
print("\n已修正event_date数据类型:", events_df['event_date'].dtype)
# 使用visitors_df.info()、events_df.info()、parks_df.info()检查其他类型
# 根据需要修正其他类型,例如,确保ID是整数(如果适用)
处理明显不正确的值,例如负数参与人数估计。我们可以将其替换为NaN或使用针对性规则。
# 处理不合理的值,如负数参与人数
print(f"\n修正前,参与人数无效的事件数:{len(events_df[events_df['attendance_est'] < 0])}")
events_df.loc[events_df['attendance_est'] < 0, 'attendance_est'] = np.nan
print(f"修正后,参与人数无效的事件数:{len(events_df[events_df['attendance_est'] < 0])}")
现在,让我们对缺失数据(NaN值)应用处理策略。
print("\n处理前的缺失值:")
print("访客:\n", visitors_df.isnull().sum())
print("事件:\n", events_df.isnull().sum())
print("公园:\n", parks_df.isnull().sum())
# visitors_df的策略:
# - age: 使用年龄中位数填充
median_age = visitors_df['age'].median()
visitors_df['age'].fillna(median_age, inplace=True)
print(f"\n已使用中位数填充缺失的'age':{median_age}")
# - gender: 根据分析需求,使用'Unknown'或众数填充
mode_gender = visitors_df['gender'].mode()[0] # 如果适用,使用众数
visitors_df['gender'].fillna('Unknown', inplace=True) # 或者使用'Unknown'填充
print(f"已使用'Unknown'填充缺失的'gender'")
# - visit_frequency: 使用众数填充
mode_freq = visitors_df['visit_frequency'].mode()[0]
visitors_df['visit_frequency'].fillna(mode_freq, inplace=True)
print(f"已使用众数填充缺失的'visit_frequency':{mode_freq}")
# events_df的策略:
# - attendance_est: 使用该事件类型的参与人数中位数填充
events_df['attendance_est'] = events_df.groupby('event_type')['attendance_est'].transform(lambda x: x.fillna(x.median()))
# 处理整个组可能都是NaN的情况
events_df['attendance_est'].fillna(events_df['attendance_est'].median(), inplace=True)
print(f"已使用事件类型内的中位数或整体中位数填充缺失的'attendance_est'。")
print("\n处理后的缺失值:")
print("访客:\n", visitors_df.isnull().sum())
print("事件:\n", events_df.isnull().sum())
让我们对访客age和公园size_acres应用缩放处理。我们将使用本章中讨论的Min-Max缩放:。
# 对访客年龄进行Min-Max缩放
min_age = visitors_df['age'].min()
max_age = visitors_df['age'].max()
visitors_df['age_scaled'] = (visitors_df['age'] - min_age) / (max_age - min_age)
print("\n已对'age'应用Min-Max缩放。")
print(visitors_df[['age', 'age_scaled']].head())
# 对公园面积进行Min-Max缩放
min_size = parks_df['size_acres'].min()
max_size = parks_df['size_acres'].max()
parks_df['size_acres_scaled'] = (parks_df['size_acres'] - min_size) / (max_size - min_size)
print("\n已对'size_acres'应用Min-Max缩放。")
print(parks_df[['name', 'size_acres', 'size_acres_scaled']].head())
# 示例:绘制缩放后年龄的分布图
import plotly.express as px
fig_age_hist = px.histogram(visitors_df, x='age_scaled', nbins=10, title='缩放后访客年龄的分布')
fig_age_hist.update_layout(bargap=0.1, xaxis_title='缩放后的年龄', yaxis_title='计数')
# 显示图表(在notebook环境中或保存为HTML)
# fig_age_hist.show()
# 对于网页输出,打印JSON表示:
# print(fig_age_hist.to_json(pretty=False)) # 这可能很长;生成一个简洁版本用于显示
# 生成用于网页显示的Plotly JSON
chart_json_output = '''
```plotly
{"layout": {"title": {"text": "缩放后访客年龄的分布"}, "bargap": 0.1, "xaxis": {"title": {"text": "缩放后的年龄"}}, "yaxis": {"title": {"text": "计数"}}}, "data": [{"type": "histogram", "x": [0.2982456140350877, 0.8421052631578947, 0.19298245614035087, 0.7192982456140351, 0.47368421052631576, 0.8596491228070176, 0.0, 0.9122807017543859, 0.49122807017543857, 0.49122807017543857, 0.2807017543859649, 0.5614035087719298, 0.6491228070175439, 0.15789473684210525, 0.05263157894736842, 0.6666666666666666, 0.9473684210526315, 0.3157894736842105, 0.3333333333333333, 0.7017543859649122, 0.49122807017543857, 0.6491228070175439, 0.40350877192982454, 0.3508771929824561, 0.7894736842105263, 0.10526315789473684, 0.7543859649122807, 0.17543859649122806, 0.2982456140350877, 0.08771929824561403, 0.49122807017543857, 0.7017543859649122, 0.017543859649122806, 0.8771929824561403, 0.7368421052631579, 0.3333333333333333, 0.42105263157894735, 0.9298245614035088, 0.12280701754385964, 0.631578947368421, 0.49122807017543857, 0.8245614035087719, 0.21052631578947367, 0.43859649122807015, 0.45614035087719296, 0.6140350877192983, 0.2807017543859649, 0.8070175438596491, 0.19298245614035087, 0.5087719298245614, 0.49122807017543857, 0.07017543859649122, 0.22807017543859648, 0.2631578947368421, 0.49122807017543857, 0.8947368421052632, 0.5789473684210527, 0.3684210526315789, 0.03508771929824561, 0.9824561403508771, 0.49122807017543857, 0.14035087719298245, 0.6842105263157894, 0.24561403508771928, 0.49122807017543857, 1.0, 0.9649122807017544, 0.5964912280701754, 0.49122807017543857, 0.2631578947368421, 0.49122807017543857, 0.5263157894736842, 0.7719298245614035, 0.38596491228070173, 0.8947368421052632, 0.49122807017543857, 0.543859649122807, 0.49122807017543857, 0.6491228070175439, 0.49122807017543857, 0.03508771929824561, 0.8771929824561403, 0.21052631578947367, 0.6140350877192983, 0.49122807017543857, 0.9122807017543859, 0.5614035087719298, 0.7368421052631579, 0.5087719298245614, 0.14035087719298245, 0.24561403508771928, 0.3684210526315789, 0.10526315789473684, 0.49122807017543857, 0.3157894736842105, 0.5964912280701754, 0.8596491228070176, 0.40350877192982454, 0.5263157894736842], "nbinsx": 10}]}
''' print(chart_json_output)
直方图显示了访客年龄在填充和Min-Max缩放后的分布。年龄现在处于0到1的范围内。
最后,让我们将这些已清理的数据集合并到一个单一的DataFrame中。我们可以根据公园ID将visitors_df与parks_df合并,并且可能也会合并事件数据,尽管这可能需要更复杂的聚合,具体取决于分析目标。现在,让我们合并访客和公园信息。
# 确保列名相同且类型兼容
# 我们之前已经将visitors_df中的'park_id'和parks_df中的'id'重命名为'park_id'了。
# 让我们确保parks_df的列名也为'park_id'以便合并。
parks_df = parks_df.rename(columns={'id': 'park_id'})
# 合并访客与公园详情数据
# 使用左合并来保留所有访客,即使他们的park_id不在parks_df中(处理潜在的不匹配)
visitor_park_merged_df = pd.merge(visitors_df, parks_df, on='park_id', how='left')
print("\n已合并的访客和公园数据:")
print(visitor_park_merged_df.head())
print(visitor_park_merged_df.info())
# 检查合并后是否有公园详情缺失(表示park_id不匹配或缺少公园)
print(f"\n合并后缺少公园信息的行数:{visitor_park_merged_df['name'].isnull().sum()}")
# 你可以进行进一步的合并,例如,按公园聚合事件参与人数并将其合并进来。
# 示例:聚合事件数据
park_event_summary = events_df.groupby('park_id').agg(
total_events=('event_id', 'count'),
avg_attendance=('attendance_est', 'mean')
).reset_index()
# 将事件汇总与主DataFrame合并
final_df = pd.merge(visitor_park_merged_df, park_event_summary, on='park_id', how='left')
# 为汇总中没有事件的公园填充NaN
final_df['total_events'].fillna(0, inplace=True)
final_df['avg_attendance'].fillna(0, inplace=True) # 或其他适当的值
print("\n包含事件汇总的最终合并数据:")
print(final_df.head())
print(final_df.info())
你现在已成功实践了本章中涵盖的数据获取和准备的核心组成部分:
现在,这个final_df DataFrame已明显更整洁、更一致,并为数据科学工作流的后续步骤(特征工程和模型构建)做好了适当的结构准备,我们将在后续章节中讨论这些内容。请记住,数据准备通常是迭代的;在分析和模型构建过程中,随着对数据了解的加深,你可能会重新审视这些步骤。
这部分内容有帮助吗?
© 2026 ApX Machine Learning用心打造