好的,让我们将本章所学的技术付诸实践。理论有价值,但将这些技能应用于真实场景可以巩固你的理解。在此动手操作部分,我们将演练一个典型的数据规整流程,模拟从不同来源收集数据、清理数据、进行转换,并将其整合为一个可供分析的数据集。假设我们正在处理与城市公园使用情况相关的数据。我们有来自问卷调查的公园访客信息(模拟数据库导出)、关于公园内特定活动的事件数据(模拟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数据库 # 在本示例中,我们假设数据是从本地文件加载的 # 模拟获取步骤的输出。步骤1:载入并查看初始数据集我们将从载入模拟数据集开始。假设我们有以下数据: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中的列名包含空格和括号。步骤2:数据清洗与标准化让我们处理上面发现的问题。标准化列名一致的列命名使合并和分析更加容易。我们会将名称转换为小写,并将空格/特殊字符替换为下划线。# 清理访客列名(大部分已清理) 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])}")步骤3:处理缺失值现在,让我们对缺失数据(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())步骤4:数据转换让我们对访客age和公园size_acres应用缩放处理。我们将使用本章中讨论的Min-Max缩放:$x_{scaled} = (x - \min(x)) / (\max(x) - \min(x))$。# 对访客年龄进行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){"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}]}直方图显示了访客年龄在填充和Min-Max缩放后的分布。年龄现在处于0到1的范围内。步骤5:合并数据集最后,让我们将这些已清理的数据集合并到一个单一的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())实践总结你现在已成功实践了本章中涵盖的数据获取和准备的核心组成部分:载入模拟不同来源(数据库、API、文件/抓取)的数据。检查数据质量问题。标准化列名以保持一致性。修正数据类型并处理错误条目。应用了处理缺失值(填充)的策略。进行了数据转换(Min-Max缩放)。将来自多个来源的数据合并到一个统一的DataFrame中。现在,这个final_df DataFrame已明显更整洁、更一致,并为数据科学工作流的后续步骤(特征工程和模型构建)做好了适当的结构准备,我们将在后续章节中讨论这些内容。请记住,数据准备通常是迭代的;在分析和模型构建过程中,随着对数据了解的加深,你可能会重新审视这些步骤。