用Python分析加州住房建设许可数据:从清洗到可视化

发布时间:2026/8/29 5:38:56
用Python分析加州住房建设许可数据:从清洗到可视化 那条新闻标题“Almost nowhere in California is building enough, according to the state”确实很吸引眼球。作为一个常年和数据打交道的开发者我看到这类标题时第一反应不是去争论政策观点而是好奇背后的数据到底长什么样、是从哪来的、能不能用代码把分析过程复现一遍。这篇博文就打算从纯技术的角度出发带你用 Python 完成“住房建设许可数据分析”的完整流程。我们会涉及到公开数据源、pandas 清洗、聚合计算和 matplotlib 可视化全程不讨论政策对错只把分析工具链路走通。无论是想练手数据分析还是想熟悉政府开放数据的处理方式这篇文章都值得收藏。1. 从新闻标题到数据分析任务1.1 一条标题背后藏着什么很多主流媒体报道政策话题时经常会引用类似“according to the state”这样的表述。这条标题的字面意思是“根据州政府的说法加州几乎没有地方在建设足够的住房”。对普通读者来说看到的是结论对技术人员来说看到的是“数据源 统计口径 分析过程”。如果我能找到州政府公布的建设许可数据理论上就能写代码验证这个结论或者至少搞清楚州政府是怎么算出这个结论的。这个场景其实非常典型新闻报道里抛出一个基于数据的结论而我们可以用公开数据重新走一遍分析路径。它既锻炼数据获取能力也锻炼数据清洗和表达能力。整件事不依赖任何敏感信息也不需要讨论具体政策的对错就是一次标准的“政府开放数据 Python 数据分析”实操。1.2 政府开放数据为什么值得技术人关注政府开放数据是指由政府机构公开发布、允许公众下载和使用的数据资源。这类数据的特点是更新频率相对规律、字段含义有官方说明、覆盖面广。比如城市规划、交通流量、教育统计、环境监测等都是很好的数据分析练手素材。尤其是城市规划相关的数据往往以 CSV、JSON 或 API 的形式提供程序员可以直接用 requests、pandas 等工具处理。同样重要的是这类数据自带“上下文”。像住房建设许可数据通常会包含地理区域、建筑类型、许可数量、统计年份等字段。字段之间的组合方式非常接近真实业务数据分析起来比纯造数据更有实感。1.3 本文要完成的分析目标我们不打算把整篇做成“爬虫 API 调用的战斗记录”因为不同数据源的接口差异很大。更稳妥的做法是先理解数据结构再准备好一份可用于教学演示的数据集然后用 pandas 完成清洗、聚合、排序和可视化。具体目标如下掌握住房建设许可数据的常见字段结构。使用 pandas 读取 CSV 并检查数据质量。学会按区域、按年份做聚合找出“建设量偏低”的地区。用 matplotlib 绘制条形图和趋势图让结论更直观。提供一套可复用的排错思路和工程建议。最后会给你一个“模拟数据 完整代码”的练习包你在本地跑起来就能看到效果。实际分析真实数据时只要替换数据源和字段名即可。2. 环境准备与版本说明2.1 开发环境怎么搭本文的代码基于 Python 3建议使用 3.9 及以上版本。如果你的机器上还没有 Python 环境推荐先安装 Anaconda 或者 Miniconda因为它们自带数据科学常用的库之后管理虚拟环境也更方便。当然纯粹使用系统 Python 加 pip 也可以并没有强制要求。如果你的环境已经是 Python 3.11 或者 3.12也不用担心本文用到的 pandas 和 matplotlib 都有对应的新版本支持。版本差异主要体现在极个别 API 的废弃与替换上面整体流程是稳定的。2.2 安装依赖库需要安装的库也不多核心是 pandas、matplotlib另外建议带上 jupyterlab方便在 Notebook 里交互式尝试。如果网络环境一般可以用国内镜像源安装速度会快很多。pip install pandas matplotlib jupyterlab如果使用 Anaconda可以这样安装conda install pandas matplotlib jupyterlab安装完成后可以用以下命令确认版本避免后续因为版本差异出现意外python -c import pandas; print(pandas.__version__) python -c import matplotlib; print(matplotlib.__version__)2.3 示例项目结构为了便于管理建议新建一个项目目录下面按照下面的结构组织文件housing_analysis/ ├── data/ │ └── california_permits_sample.csv ├── scripts/ │ └── analysis.py ├── output/ │ └── (生成的图表) └── README.mddata目录存放数据文件scripts目录存放 Python 脚本output目录存放图表结果。这样拆分的目的是让数据处理、代码和结果互不干扰也方便以后替换成其他真实数据集。3. 数据获取公开数据源与数据格式说明3.1 常见公开数据源如果以后你想获取真实的美国住房建设许可数据可以关注下面几类公开来源。请注意数据源的具体网址和接口参数可能会随官方改版而变化使用前务必以官方文档为准。美国人口普查局的 Building Permits Survey这是美国范围内比较权威的新建住宅建筑许可统计覆盖州、县、城市等级别。加州开放数据门户许多州都有自己的开放数据平台可以直接下载 CSV 或通过 API 查询。地方市政府数据中心部分大城市会有更细颗粒度的许可数据但格式差异较大。由于真实数据的下载和认证流程一直在变本文不写死某一个接口地址而是提供一份模拟数据结构上尽量贴近真实数据。你把流程跑通之后再去替换真实数据源会顺畅很多。3.2 数据字典说明模拟数据保存在data/california_permits_sample.csv中核心字段定义如下字段名类型字段含义citystr城市名称countystr所属县yearint统计年份total_unitsint当年批准的新建住房单元数量single_familyint独栋住宅许可数量multi_familyint多户住宅许可数量区分total_units和single_family、multi_family是为了后续可以做结构分析。比如某个区域总量不低但可能全部是多户住宅独栋住宅很少这类信息只有在字段拆细之后才能看出来。下面我们人为构造一段样例数据这里要特别说明该数据仅用于教学演示不代表任何真实统计结果。city,county,year,total_units,single_family,multi_family Los Angeles,Los Angeles County,2020,18000,4500,13500 San Diego,San Diego County,2020,7200,2100,5100 San Jose,Santa Clara County,2020,5100,1600,3500 Sacramento,Sacramento County,2020,4300,1100,3200 San Francisco,San Francisco County,2020,3900,600,3300 Fresno,Fresno County,2020,3600,900,2700 Long Beach,Los Angeles County,2020,2400,700,1700 Oakland,Alameda County,2020,3100,800,2300 ...实际真实数据中列名可能叫Units、Permits、ReportingPeriod之类的名字因此清洗数据的第一步永远是“先看字段名和字段含义”不要急着写聚合代码。3.3 手动准备数据文件上面的演示数据需要你自己复制保存为 CSV 文件。为了练习效果更完整建议把时间跨度设置为 2018 到 2023 年覆盖 12 到 15 个城市。字段不要贪多重点是city、county、year、total_units。如果你手头已经有真实数据那就更好直接跳到下一节。4. 使用 pandas 进行数据清洗与预处理4.1 读取数据与初步探查拿到 CSV 文件后第一件事不是马上算平均值而是先看数据长什么样。用pd.read_csv()读取数据再用head()、info()、describe()观察数据的基本情况。代码如下import pandas as pd df pd.read_csv(data/california_permits_sample.csv) print(df.head()) print(\n数据基本信息) print(df.info()) print(\n数值列统计信息) print(df.describe())输出效果大致如下city county year total_units single_family multi_family 0 Los Angeles Los Angeles County 2020 18000 4500 13500 1 San Diego San Diego County 2020 7200 2100 5100 ...通过info()可以快速发现两个问题是否有缺失值、数据类型是否合理。比如如果year被读成了字符串而不是整数后续按年份筛选时会出错所以这一步非常关键。4.2 缺失值与重复值处理现实数据集很少是干净的常见问题包括缺失值、重复行、列名带空格、数值列被读成字符串等。模拟数据里不一定都有这些问题但处理思路要讲清楚。# 查看缺失值情况 print(df.isna().sum()) # 查看重复行数量 print(df.duplicated().sum()) # 如果有重复行直接删除 df df.drop_duplicates()对于缺失值不要一刀切地删除要看缺失的业务含义。比如multi_family缺失可能表示当年没有多户住宅许可也可能是数据漏录。如果无法确认最稳妥的方式是标记出来而不是用平均值去填。平均值填充会给后续分析引入伪信息。4.3 列名规范化政府开放数据里列名经常带空格、大写、特殊字符。为了方便后续代码编写建议统一改成小写并去掉空格。df.columns df.columns.str.strip().str.lower().str.replace( , _)例如把City变成city把Total Units变成total_units。这一步虽小但能大幅减少后续写代码时的报错概率。4.4 数据类型转换检查数据类型的下一步是修正明显不合理的类型。比如year可能是浮点数或者total_units因为某些原因被读成了字符串。可以用pd.to_numeric()做安全转换并设置errorscoerce让转换不了的值变成NaN方便接着排查。df[year] pd.to_numeric(df[year], errorscoerce) df[total_units] pd.to_numeric(df[total_units], errorscoerce) df[single_family] pd.to_numeric(df[single_family], errorscoerce) df[multi_family] pd.to_numeric(df[multi_family], errorscoerce)转换后再次检查isna()如果某个字段出现大量NaN就要回到原始数据文件去确认原因了。5. 数据分析找出“建得不够”的地区5.1 按年份汇总先看整体趋势是最直观的。把数据按年份分组求和可以看到总建设许可量的变化。这能帮你判断问题是不是“最近一两年确实在减少”还是“一直都不多”。yearly_total df.groupby(year)[total_units].sum() print(yearly_total)输出类似year 2018 48000 2019 51000 2020 52000 2021 49000 2022 46000 2023 43000 Name: total_units, dtype: int64如果真实数据也呈现下降趋势那就说明新闻标题里的“建设不够”可能和近年增速放缓有关。当然只看总量还不够还要看不同城市的分化。5.2 按城市聚合排序“哪里最缺”不能只凭感觉判断要用数据排序。这里以城市为分组对象计算每个城市近几年的平均许可量然后升序排列排在最前面的就是平均建设量最少的城市。city_mean df.groupby(city)[total_units].mean().sort_values() print(city_mean.head(10))如果想看每个城市最近一年的数据和前几年均值之间的差距可以用透视表pivot df.pivot_table( indexcity, columnsyear, valuestotal_units, aggfuncsum ) print(pivot)这个透视表能直观地看到每个城市在不同年份的建设量变化比只看平均值信息量更大。5.3 多指标对比光看总量可能掩盖结构问题。我们可以进一步计算独栋住宅许可占比或者多户住宅与独栋住宅的比值df[single_family_ratio] df[single_family] / df[total_units] city_ratio df.groupby(city)[single_family_ratio].mean().sort_values() print(city_ratio.head(10))这个指标的意义在于有些城市建设总量不低但可能是高层公寓为主独栋住宅很少。如果新闻中谈的是“住房可负担性”单看总量还不够需要结构层面的分析。5.4 结合人口数据做相对指标更深入一点可以把建设许可数据和地区人口数结合计算人均许可量。比如人均许可量 总许可量 / 人口数 * 10000这比绝对数量更能反映“够不够”。由于演示数据里没有人口字段这里给出思路如下# 假设有一个 population.csv包含 city 和 population 字段 # population pd.read_csv(data/population.csv) # merged df.merge(population, oncity, howleft) # merged[permits_per_10k] merged[total_units] / merged[population] * 10000真实项目里人口数据同样可以从公开数据源获取合并时注意城市名称是否一致必要时先做名称标准化。6. 可视化呈现分析结果6.1 建设量最低的 10 个城市数据计算完成后图表是最好的表达方式。我们先画一张横向条形图展示平均建设量最低的 10 个城市。import matplotlib.pyplot as plt city_mean df.groupby(city)[total_units].mean().sort_values() top10_low city_mean.head(10) plt.figure(figsize(10, 6)) top10_low.plot(kindbarh, color#4C72B0) plt.xlabel(Average Total Units) plt.ylabel(City) plt.title(Bottom 10 Cities by Average Housing Units) plt.tight_layout() plt.savefig(output/bottom10_cities.png, dpi150) plt.show()横向条形图适合展示排名城市名称较长时也不会互相遮挡。6.2 时间趋势图如果需要观察“近几年的建设量是否在减少”折线图更合适。用透视表计算每个城市每年的总量然后绘制多条折线。pivot df.pivot_table( indexyear, columnscity, valuestotal_units, aggfuncsum ) plt.figure(figsize(12, 6)) for city in pivot.columns: plt.plot(pivot.index, pivot[city], markero, labelcity) plt.xlabel(Year) plt.ylabel(Total Units) plt.title(Housing Units Trend by City) plt.legend(bbox_to_anchor(1.02, 1), locupper left) plt.grid(alpha0.3) plt.tight_layout() plt.savefig(output/trend_by_city.png, dpi150) plt.show()如果城市数量很多折线图会变得混乱建议只选择若干个重点城市单独绘制或者使用分面图facet展示。真实项目中图表不是越复杂越好能说明问题的才是好的。6.3 中国开发者常见的中文乱码问题在绘图时如果标题或坐标轴使用了中文很容易出现方框乱码。这是因为 matplotlib 默认字体不支持中文。解决办法是显式指定中文字体下面以 macOS 和 Windows 的常见字体为例。plt.rcParams[font.sans-serif] [Microsoft YaHei, SimHei, PingFang SC] plt.rcParams[axes.unicode_minus] False第二行axes.unicode_minus是用于修复坐标轴负号显示为方块的问题。配置后重新绘图即可。不过要提醒一点在生成可分享的图表时建议保留英文标题或者在图表下方用文字解释指标含义这样兼容性更好。7. 常见问题与排查思路7.1 CSV 读取后中文乱码问题现象常见原因解决思路打印数据或绘图时中文乱码文件编码与 pandas 默认编码不一致在read_csv()中指定编码例如encodingutf-8-sig或encodinggbk控制台显示中文乱码终端本身编码不支持在脚本开头设置标准输出编码或直接查看数据时另存为 UTF-8读取 CSV 时如果发现中文列名乱码可以先尝试df pd.read_csv(data/california_permits_sample.csv, encodingutf-8-sig)utf-8-sig能处理带 BOM 的 UTF-8 文件是政府数据下载中比较常见的情况。7.2KeyError列名找不到这个问题多起因于列名带有空格、大小写不同或者根本没有那一列。排查时先打印所有列名print(df.columns.tolist())然后根据实际列名调整代码。如果你用的是我在 4.3 里提到的列名规范化函数之后大部分KeyError都能避免。7.3 数据类型导致聚合结果有误如果total_units被读成了字符串groupby().sum()会报错或者直接做字符串拼接结果非常奇怪。解决方法是用pd.to_numeric()转换。如果转换后出现大量NaN说明数据原始文件里可能混入了字符值需要用df[df[total_units].isna()]定位具体行。7.4 API 请求被限流或返回格式变化调用政府开放数据 API 时可能遇到限流、鉴权或字段版本变化。建议在代码中做好用户代理标识和请求间隔同时把请求结果缓存到本地避免重复请求。对于不稳定的字段用字典映射做兼容而不是硬编码列名。7.5 图表尺寸过大或显示不全绘制几十个城市的趋势图容易导致图例占据整个画面。解决方法只绘制重点城市。用figsize调大画布。把图例放到图外如bbox_to_anchor(1.02, 1)。使用多个子图分开展示。8. 最佳实践与工程建议8.1 数据文件与代码分离真实项目中不要把数据直接硬编码在脚本里更不要手动复制到代码中。建议建立清晰的数据目录并维护一份数据字典文档。如果数据是通过脚本生成的还要把生成脚本一起保存保证数据可追溯。8.2 使用配置管理数据源信息数据源的 URL 或文件路径不要散落在代码各处。可以用一个简单的config.py或 YAML 文件统一管理。比如# config.py DATA_PATH data/california_permits_sample.csv OUTPUT_DIR output FIG_SIZE (10, 6)这样改路径或调整参数时不需要翻遍整个项目。8.3 分析脚本要有阶段划分建议把分析过程拆成几步而不是一个脚本写到底。比如load_data.py负责读取数据。clean_data.py负责清洗。analysis.py负责计算指标。visualize.py负责出图。每个函数只做一件事方便单测也方便复用。如果只是在 Jupyter Notebook 里做探索性分析则建议按单元格划分并在开头写清依赖说明。8.4 绘图一致性同一份分析报告里图表风格应该保持一致。可以预定义颜色、字号和画布尺寸。比如COLORS [#4C72B0, #55A868, #C44E52, #8172B2] plt.rcParams[font.size] 12 plt.rcParams[figure.dpi] 100这样既美观又减少了重复工作。8.5 客观看待数据分析结论数据分析工具只能告诉我们“数据呈现了什么”不能直接证明政策或者新闻论断的对错。做分析时可以分两步思考描述性分析哪些地方建设量低、趋势是怎么变的。解释性分析尝试用人口、经济、土地等因素解释差异但这需要更多数据支撑。在写结论时尽量使用“数据显示”“从当前数据来看”这类限定语而不是“事实证明”。这样可以有效避免过度解读。8.6 数据更新与自动化真实的政府开放数据通常周期性更新。如果你希望保持分析结论不过时可以把数据处理脚本做成定时任务。比如用 GitHub Actions 或者本机 cron 定时拉取最新数据并重新生成图表。这种自动化能力在数据分析项目中很加分。9. 总结与下一步学习路线到此为止我们已经把“从新闻标题到数据分析”的完整流程走了一遍。首先是理解数据字段然后用 pandas 读取和清洗数据接着通过聚合分析找出平均建设量偏低和趋势下降明显的城市最后用 matplotlib 生成直观的图表。整个过程不依赖昂贵的软件或专门的数据分析平台只要本地有 Python 环境就能完成。下一步你可以尝试几个方向把模拟数据替换成真实的州政府开放数据体验“真实数据 vs 演示数据”的差异。引入人口数据计算人均住房建设许可量增加分析维度。学习更多 pandas 聚合函数例如rolling、pct_change分析年度变化率。尝试用交互式可视化库比如 Plotly 或 pyecharts让图表支持鼠标悬停和筛选。如果你更关注数据分析中的工程化能力还可以学习数据版本管理工具 DVC、工作流框架 Prefect 或 Airflow把这份分析和定时调度结合起来。数据分析的入门门槛并不高难的是养成“先理解字段、再处理缺失、最后谨慎下结论”的习惯。希望这篇教程对你有切实帮助如果你跑完代码有什么问题也欢迎对照本文的排错清单自查一遍大概率能省下不少调试时间。

相关新闻