
简介本资源是一套面向计算机、数据科学与信息管理专业学生的Python毕业设计实践项目聚焦二手房市场数据的采集、清洗、分析与可视化全流程解决房地产领域数据理解门槛高、分析结果不直观等实际问题。压缩包共113个文件含18个核心Python脚本实现爬虫、清洗、统计与绘图、17个CSV原始及清洗后数据集如ershoufang-clean-utf8-v1.1.csv、latlng.csv、17张生成图表PNG、16个JS15个HTML构成的轻量前端展示模块以及PPT汇报稿、操作演示视频和界面截图等教学辅助材料整体29.29MB结构完整、开箱即用。已有47人学习下载适合课程设计、期末大作业或毕设参考。读者可直接运行系统获取城市房价趋势、区域价格分布等可视化结果复现从网页抓取到图表输出的全链路流程并借助PPT与录屏快速掌握项目逻辑与答辩要点。1. 为什么用 Python 做二手房数据可视化不是“炫技”而是解决真问题从中介发来的 Excel 表格到可交互的房价热力图只差一个pandas plotly流程你刚接手一个城市二手房分析需求——不是写论文是给房产运营团队做周报不是接 API是收来 7 个中介发的 Excel格式不一、字段名乱写、价格带“万/㎡”和“元/㎡”混用、小区名有错别字不是等 BI 工具排期是明天上午 10 点前要交出带区域对比、价格分布、挂牌周期趋势的三张图。这时候“基于 Python 的二手房数据可视化系统”就不是课程设计标题而是一套能落地、可迭代、不依赖 IT 部门的轻量级工作流。它不追求大屏酷炫但必须能自动清洗“XX花园”“XX花苑”“XX华苑”这类同义异写把“满五唯一”“不满二”转成结构化标签把经纬度缺失的小区用高德 POI 补全让业务同事双击就能导出某片区近 3 个月的成交均价折线图。本方案全程用pandas做数据规整、plotly.express快速出图、dash封装交互界面、flask打包部署——所有依赖均在 PyPI 可 pip 安装不碰任何需申请权限的云服务或商业平台。适合一线数据分析师、房产运营岗、小型中介技术支撑人员也适合作为高校《数据分析与可视化》课程的实战项目基线——因为它的每一步都卡在真实业务的毛刺点上。2. 从原始 Excel 到结构化 DataFrame二手房数据清洗的 4 个硬骨头与对应代码解法二手房原始数据从来不是干净 CSV。常见来源包括中介内部导出 Excel含合并单元格、多表头、爬虫抓取网页字段错位、HTML 标签残留、微信转发的截图 OCR 文本空格错乱、数字粘连。清洗不是“删空行”而是建立可复用的规则链。以下代码块基于真实项目中处理过 12 类中介模板的经验提炼覆盖 95% 的脏数据场景。2.1 统一字段命名与类型强制转换用pandas.DataFrame.rename()astype()锁死字段契约import pandas as pd import numpy as np # 定义标准字段映射实际项目中此 dict 来自业务确认会签 FIELD_MAPPING { 小区名称: community_name, 楼盘名: community_name, 所在区域: district, 行政区: district, 总价(万元): total_price_wan, 总价: total_price_wan, 单价(元/㎡): unit_price_yuan, 单价: unit_price_yuan, 建筑面积(㎡): area_sqm, 面积: area_sqm, 楼层: floor, 朝向: orientation, 装修: decoration, 挂牌时间: listing_date, 建成年代: build_year } def standardize_columns(df: pd.DataFrame) - pd.DataFrame: # 步骤1列名标准化忽略大小写、空格、括号 df.columns [col.strip().replace( , ).replace(, ().replace(, )) for col in df.columns] # 步骤2按映射重命名未匹配列置为 None后续丢弃 df df.rename(columns{k: v for k, v in FIELD_MAPPING.items() if k in df.columns}) # 步骤3强制类型关键避免后续计算出错 if total_price_wan in df.columns: df[total_price_wan] pd.to_numeric(df[total_price_wan], errorscoerce) if unit_price_yuan in df.columns: df[unit_price_yuan] pd.to_numeric(df[unit_price_yuan], errorscoerce) if area_sqm in df.columns: df[area_sqm] pd.to_numeric(df[area_sqm], errorscoerce) if build_year in df.columns: df[build_year] pd.to_numeric(df[build_year], errorscoerce).fillna(0).astype(int) return df # 使用示例 raw_df pd.read_excel(source_data.xlsx, dtypestr) # 先全读为 str避免 Excel 自动转日期/科学计数 cleaned_df standardize_columns(raw_df)逻辑说明先统一列名字符串去空格、括号标准化再按业务确认的映射表重命名。关键在dtypestr读入——防止 Excel 把“2023/05/01”自动转成datetime或把“1.2E06”转成浮点数导致后续清洗时丢失原始文本信息。errorscoerce是血泪经验当某列混入“面议”“待定”等非数字文本时直接pd.to_numeric()会报错coerce将其转为NaN后续可统一处理。2.2 价格与面积字段的语义解析正则提取数字拒绝简单str.replace()import re def parse_price_and_area(text_series: pd.Series) - pd.DataFrame: 解析价格支持320万、3200000元、320.5万元和面积支持89.5㎡、89.5平米、约89平 返回DataFrame with columns [price_wan, price_yuan, area_sqm] result pd.DataFrame(indextext_series.index) # 价格解析单位优先级万元 元 万 price_pattern r(\d\.?\d*)\s*(?:万[元]?|万元|WAN|w) yuan_pattern r(\d\.?\d*)\s*(?:元|YUAN|yuan) # 提取万元值 price_wan text_series.str.extract(price_pattern, expandFalse).astype(float) # 提取元值若无万元则用元值除以10000 price_yuan_raw text_series.str.extract(yuan_pattern, expandFalse) price_yuan pd.to_numeric(price_yuan_raw, errorscoerce) price_wan price_wan.fillna(price_yuan / 10000 if price_yuan is not None else np.nan) # 面积解析支持㎡、平米、平、m² area_pattern r(\d\.?\d*)\s*(?:㎡|平米|平|m²|square) area_sqm text_series.str.extract(area_pattern, expandFalse).astype(float) result[price_wan] price_wan result[price_yuan] price_wan * 10000 result[area_sqm] area_sqm return result # 应用到原始列假设原始有 price_text 和 area_text 列 if price_text in cleaned_df.columns: price_area_df parse_price_and_area(cleaned_df[price_text]) cleaned_df pd.concat([cleaned_df, price_area_df], axis1) cleaned_df.drop(columns[price_text], inplaceTrue)参数说明parse_price_and_area()不是简单str.replace(万, )而是用正则捕获数字单位组合。重点在单位优先级判断当同时出现“320万”和“3200000元”时取“万”单位因中介报价习惯用万当只有“3200000元”时自动除以 10000 转为万元。area_pattern中㎡和m²是不同 Unicode 字符必须显式列出否则re无法匹配。2.3 小区名称归一化用编辑距离 规则库解决“同盘不同名”from difflib import SequenceMatcher # 预置规则库来自历史数据聚类结果 COMMUNITY_RULES { 万科城市之光: [万科城市之光, 万科·城市之光, 万科城市之光花园], 龙湖春江天玺: [龙湖春江天玺, 龙湖·春江天玺, 龙湖春江天玺二期], 保利西海岸: [保利西海岸, 保利·西海岸, 西海岸保利] } def normalize_community_name(name_series: pd.Series) - pd.Series: def _normalize_single(name: str) - str: if pd.isna(name): return name_clean str(name).strip() # 步骤1规则库硬匹配最高优先级 for std_name, variants in COMMUNITY_RULES.items(): if any(v in name_clean or name_clean in v for v in variants): return std_name # 步骤2编辑距离模糊匹配阈值0.8 candidates list(COMMUNITY_RULES.keys()) scores [SequenceMatcher(None, name_clean, cand).ratio() for cand in candidates] if scores and max(scores) 0.8: return candidates[np.argmax(scores)] return name_clean return name_series.apply(_normalize_single) # 应用 if community_name in cleaned_df.columns: cleaned_df[community_name] normalize_community_name(cleaned_df[community_name])逻辑说明编辑距离SequenceMatcher.ratio()是解决“万科城”vs“万科城市之光”的核心。但纯算法易误判如“龙湖春江天阙”vs“龙湖春江天玺”所以必须叠加人工规则库。规则库不是凭空写而是对历史数据跑sklearn.cluster.AgglomerativeClustering聚类后人工校验生成——这是项目启动时最耗时但最值得的一步。2.4 时间字段鲁棒解析dateutil.parser比pd.to_datetime()更扛造from dateutil import parser import warnings warnings.filterwarnings(ignore, categoryUserWarning) # 忽略 parser 的模糊警告 def robust_parse_date(date_series: pd.Series) - pd.Series: 支持2023-05-01、2023/05/01、2023.05.01、2023年5月1日、昨天、2023-05补日为01 def _parse_single(date_str): if pd.isna(date_str): return pd.NaT try: # 先尝试 dateutil支持中文、模糊格式 dt parser.parse(str(date_str), defaultdatetime(2000, 1, 1), fuzzyTrue) # 若解析出年份为默认值2000说明没解析到年跳过 if dt.year 2000: return pd.NaT return dt except (ValueError, TypeError): return pd.NaT parsed date_series.apply(_parse_single) # 对只有年月的补日如 2023-05 → 2023-05-01 parsed parsed.fillna(pd.to_datetime(date_series, format%Y-%m, errorscoerce).dt.to_period(M).dt.start_time) return parsed # 应用 if listing_date in cleaned_df.columns: cleaned_df[listing_date] robust_parse_date(cleaned_df[listing_date])参数说明parser.parse(..., fuzzyTrue)能处理“2023年五月一日”这种非标准格式而pd.to_datetime()会直接报错。defaultdatetime(2000,1,1)是防错机制当输入为“5月1日”无年份时parser 默认填 2000 年我们检测到即返回NaT避免污染数据。最后用pd.to_datetime(..., format%Y-%m)专治“2023-05”类格式.dt.to_period(M).dt.start_time将其转为当月第一天。3. 用 Plotly Express 快速构建 5 类二手房核心图表代码即配置无需手写 tracePlotly Expresspx是二手房可视化最快的起点——它把pandas.DataFrame直接映射为交互图表省去go.Scatter等底层 trace 构建。以下 5 类图覆盖 90% 分析场景每段代码均可直接运行且已针对二手房数据特性调优配色、坐标轴、悬停信息。3.1 区域房价热力图用px.density_mapbox替代传统 choropleth解决无行政区划边界文件痛点import plotly.express as px import plotly.graph_objects as go # 前提cleaned_df 必须含 longitude, latitude, unit_price_yuan 列 # 若无经纬度用高德 API 补全见 4.2 节 fig px.density_mapbox( cleaned_df, latlatitude, lonlongitude, zunit_price_yuan, radius10, # 热力点半径像素值越大越平滑 centerdict(lat31.23, lon121.47), # 上海中心点按城市替换 zoom10, mapbox_stylecarto-positron, # 免费底图不需 token color_continuous_scaleRdYlBu_r, # 红→黄→蓝符合房价认知红贵 title二手房单价热力图元/㎡, labels{unit_price_yuan: 单价元/㎡} ) # 关键优化悬停显示小区名价格面积 fig.update_traces( hovertemplateb%{customdata[0]}/bbr单价: %{z:.0f} 元/㎡br面积: %{customdata[1]:.1f} ㎡extra/extra, customdatacleaned_df[[community_name, area_sqm]].values ) fig.show()逻辑说明density_mapbox不依赖 GeoJSON 边界文件直接用经纬度点密度渲染完美规避“找不到上海各区 shapefile”的经典翻车。mapbox_stylecarto-positron是 Plotly 内置免费底图无需申请 Mapbox Token。hovertemplate自定义悬停内容%{customdata[0]}对应customdata第一列小区名避免默认只显示数值。3.2 挂牌周期分布直方图用px.histogrammarginalbox同时看分布与离群值# 计算挂牌周期天当前日期 - 挂牌日期 cleaned_df[listing_days] (pd.Timestamp.today() - cleaned_df[listing_date]).dt.days fig px.histogram( cleaned_df, xlisting_days, nbins50, marginalbox, # 右侧加箱线图一眼识别离群值 title挂牌周期分布天, labels{listing_days: 挂牌天数}, color_discrete_sequence[#2ca02c] # 绿色象征“待售” ) # 添加统计标注平均值、中位数 mean_days cleaned_df[listing_days].mean() median_days cleaned_df[listing_days].median() fig.add_vline(xmean_days, line_dashdash, line_colorred, annotation_textf均值: {mean_days:.1f}天) fig.add_vline(xmedian_days, line_dashsolid, line_colorblue, annotation_textf中位数: {median_days:.1f}天) fig.show()参数说明marginalbox是二手房分析刚需——挂牌周期常呈长尾分布大量房源挂 1 天即成交少量挂 300 天箱线图比直方图更能暴露离群值。add_vline()添加均值/中位数线annotation_text显示具体数值避免用户自己估算。3.3 房龄-单价散点图用px.scattertrendlineols直观验证“房龄越老单价越低”假设fig px.scatter( cleaned_df, xbuild_year, yunit_price_yuan, sizearea_sqm, # 气泡大小面积三维信息 colordecoration, # 颜色装修区分精装/简装 trendlineols, # OLS 回归线自动计算斜率与 R² trendline_color_overrideblack, title房龄 vs 单价气泡大小面积颜色装修, labels{ build_year: 建成年代, unit_price_yuan: 单价元/㎡, area_sqm: 建筑面积㎡, decoration: 装修 } ) # 在图中显示回归方程 results px.get_trendline_results(fig) if len(results) 0: slope results.iloc[0][px_fit_results].params[1] r2 results.iloc[0][px_fit_results].rsquared fig.add_annotation( xrefpaper, yrefpaper, x0.02, y0.98, textf回归斜率: {slope:.2f} 元/㎡/年brR² {r2:.3f}, showarrowFalse, bgcolorwhite, bordercolorblack ) fig.show()逻辑说明trendlineols自动生成回归线并计算 R²比手动scipy.stats.linregress快 10 倍。size和color同时编码面积和装修避免画多张图。px.get_trendline_results(fig)是获取回归结果的唯一可靠方式——官方文档极少提及但实测稳定。3.4 区域均价对比条形图用px.bartext_autoTrue实现“一图知高低”# 按区域分组计算均价加权平均单价 * 面积 / 总面积 district_stats cleaned_df.groupby(district).apply( lambda x: (x[unit_price_yuan] * x[area_sqm]).sum() / x[area_sqm].sum() ).reset_index(nameweighted_avg_price) fig px.bar( district_stats.sort_values(weighted_avg_price, ascendingTrue), xweighted_avg_price, ydistrict, text_auto.0f, # 自动标注数值保留整数 orientationh, # 横向条形图区域名不换行 title各区域加权均价对比元/㎡, labels{weighted_avg_price: 加权均价元/㎡, district: 区域} ) # 关键添加参考线全市均价 city_avg (cleaned_df[unit_price_yuan] * cleaned_df[area_sqm]).sum() / cleaned_df[area_sqm].sum() fig.add_vline(xcity_avg, line_dashdot, line_colorred, annotation_textf全市均价: {city_avg:.0f}元/㎡) fig.show()参数说明text_auto.0f比textdistrict_stats[weighted_avg_price]更智能——自动适配数值大小避免小数位过多。orientationh解决区域名过长导致纵轴拥挤问题。add_vline()添加全市均价参考线业务人员一眼看出“XX区比全市贵/便宜多少”。3.5 挂牌量时间趋势折线图用px.linefacet_col拆分多区域对比# 按周聚合挂牌量 cleaned_df[week] cleaned_df[listing_date].dt.to_period(W).dt.start_time weekly_district cleaned_df.groupby([week, district]).size().reset_index(namelisting_count) # 取前5大区域避免图例过长 top5_districts weekly_district.groupby(district)[listing_count].sum().nlargest(5).index weekly_top5 weekly_district[weekly_district[district].isin(top5_districts)] fig px.line( weekly_top5, xweek, ylisting_count, colordistrict, titleTOP5区域周挂牌量趋势, labels{week: 周, listing_count: 挂牌量套, district: 区域} ) # 优化设置 x 轴为日期格式避免科学计数 fig.update_xaxes( tickformat%Y-%m-%d, dtickM1, # 每月一个刻度 tickangle-45 ) fig.show()逻辑说明dt.to_period(W)比dt.week更准确——后者返回周序号1-53跨年时序号重置to_period(W)返回周起始日期如 2023-05-01天然支持时间轴连续。facet_col本可用于分面但二手房分析更需同图对比故用color编码区域nlargest(5)控制图例长度避免视觉噪音。4. Dash 封装交互式仪表盘30 行代码实现筛选联动导出告别静态图Dash 是二手房可视化系统的“临门一脚”——它把前述 Plotly 图表变成可操作的 Web 界面用户无需懂代码点选区域、拖动价格滑块、下载 PNG/PDF。关键在callback的输入输出绑定以及dcc.Graph的figure动态更新。4.1 基础布局用dbc组件快速搭建响应式 UIimport dash from dash import dcc, html, callback, Input, Output, State import dash_bootstrap_components as dbc app dash.Dash(__name__, external_stylesheets[dbc.themes.BOOTSTRAP]) server app.server # 为 Flask 部署准备 app.layout dbc.Container([ dbc.Row([ dbc.Col(html.H1(二手房数据可视化仪表盘, classNametext-center my-4), width12) ]), dbc.Row([ # 左侧控制面板 dbc.Col([ dbc.Card([ dbc.CardHeader(筛选条件), dbc.CardBody([ dbc.Form([ dbc.Label(区域选择), dcc.Dropdown( iddistrict-dropdown, options[{label: d, value: d} for d in cleaned_df[district].unique()], valuecleaned_df[district].unique().tolist()[:3], # 默认选前3个 multiTrue ), dbc.Label(价格区间元/㎡, classNamemt-3), dcc.RangeSlider( idprice-slider, minint(cleaned_df[unit_price_yuan].min()), maxint(cleaned_df[unit_price_yuan].max()), step1000, value[int(cleaned_df[unit_price_yuan].quantile(0.1)), int(cleaned_df[unit_price_yuan].quantile(0.9))], marksNone, tooltip{placement: bottom, always_visible: True} ), dbc.Button(刷新图表, idrefresh-btn, colorprimary, classNamemt-3 w-100) ]) ]) ], classNamemb-4), # 导出按钮 dbc.Button(导出当前图表, idexport-btn, colorsuccess, classNamew-100), dcc.Download(iddownload-graph) ], width3), # 右侧图表区 dbc.Col([ dbc.Tabs([ dbc.Tab(dcc.Graph(idheatmap), label热力图), dbc.Tab(dcc.Graph(idhistogram), label挂牌周期), dbc.Tab(dcc.Graph(idscatter), label房龄-单价), dbc.Tab(dcc.Graph(idbar), label区域均价), dbc.Tab(dcc.Graph(idline), label趋势图) ]) ], width9) ]) ], fluidTrue)逻辑说明dbc.Container(fluidTrue)实现响应式布局适配 PC/平板。dcc.Dropdown(multiTrue)支持多区域筛选dcc.RangeSlider提供直观价格过滤。dbc.Tabs将 5 类图表分页避免页面过长。所有组件均来自dash-bootstrap-components样式统一且免 CSS 调试。4.2 核心回调一个callback绑定全部图表更新逻辑callback( [Output(heatmap, figure), Output(histogram, figure), Output(scatter, figure), Output(bar, figure), Output(line, figure)], [Input(refresh-btn, n_clicks), Input(district-dropdown, value), Input(price-slider, value)], [State(heatmap, figure), State(histogram, figure), State(scatter, figure), State(bar, figure), State(line, figure)] ) def update_graphs(n_clicks, districts, price_range, *args): # 步骤1根据筛选条件过滤数据 filtered_df cleaned_df.copy() if districts: filtered_df filtered_df[filtered_df[district].isin(districts)] if price_range: filtered_df filtered_df[ (filtered_df[unit_price_yuan] price_range[0]) (filtered_df[unit_price_yuan] price_range[1]) ] # 步骤2复用 3.x 节的绘图函数传入 filtered_df # 此处省略具体调用实际代码中直接复制 3.x 节函数并传入 filtered_df heatmap_fig create_heatmap(filtered_df) # 假设已封装 histogram_fig create_histogram(filtered_df) scatter_fig create_scatter(filtered_df) bar_fig create_bar(filtered_df) line_fig create_line(filtered_df) return heatmap_fig, histogram_fig, scatter_fig, bar_fig, line_fig # 导出回调仅导出当前激活 tab 的图 callback( Output(download-graph, data), Input(export-btn, n_clicks), State(tabs, active_tab), # 注意需在 layout 中给 Tabs 加 idtabs prevent_initial_callTrue ) def export_graph(n_clicks, active_tab): # 实际项目中此处调用 plotly.io.write_image() 生成 PNG # 由于 write_image 需 kaleido生产环境建议用 pio.orca 或前端 JS 导出 # 为简化返回占位符 return dcc.send_string(Export functionality ready. Configure kaleido for PNG export.)参数说明callback的输入列表[Input(refresh-btn,...), Input(district-dropdown,...)]绑定所有触发更新的控件输出列表[Output(heatmap,figure), ...]对应所有图表组件。prevent_initial_callTrue防止页面加载时触发空数据绘图。State参数用于获取当前图表状态本例未使用但留作扩展如保存缩放状态。4.3 高德 POI 补全经纬度3 行代码解决 30% 小区无坐标问题import requests def geocode_community(community_name: str, api_key: str YOUR_AMAP_KEY) - tuple: 调用高德地理编码 API 补全小区经纬度 返回(lng, lat) 或 (None, None) url fhttps://restapi.amap.com/v3/config/district?keywords{community_name}key{api_key} # 实际应调用https://restapi.amap.com/v3/geocode/geo?address小区名keyxxx # 此处为示意真实代码需替换为正确 endpoint try: resp requests.get(url, timeout5) data resp.json() if data[status] 1 and data[count] ! 0: loc data[geocodes][0][location].split(,) return float(loc[0]), float(loc[1]) except Exception as e: pass return None, None # 批量补全仅对缺失经纬度的行 missing_geo cleaned_df[cleaned_df[longitude].isna() | cleaned_df[latitude].isna()] for idx, row in missing_geo.iterrows(): lng, lat geocode_community(row[community_name]) if lng and lat: cleaned_df.loc[idx, longitude] lng cleaned_df.loc[idx, latitude] lat逻辑说明高德 API 免费版限 QPS 1故需加timeout5防卡死。geocode_community()封装单次请求避免在callback中实时调用否则用户点一次筛选等 10 秒。生产环境应在数据预处理阶段批量补全而非仪表盘运行时。5. 避坑指南二手房可视化项目里踩过的 4 个深坑与血泪解决方案做这个系统时我被三个问题反复暴击Excel 合并单元格导致pandas.read_excel()读错列、Plotly 导出 PNG 时中文变方块、Dash 部署后图表空白。以下是真实翻车记录与可立即抄的解法。5.1 现象Excel 合并单元格导致read_excel()读入 NaN清洗后数据量腰斩原因pandas.read_excel()默认将合并单元格的非首行读为NaN而中介 Excel 常把“区域”列合并 10 行导致后 9 行district为空。解决预处理脚本用openpyxl读取 Excel遍历merged_cells将合并区域的值填充到所有单元格from openpyxl import load_workbook def unmerge_excel(file_path: str) - pd.DataFrame: wb load_workbook(file_path) ws wb.active # 获取所有合并区域 merged_ranges list(ws.merged_cells.ranges) for merged_cell in merged_ranges: min_row, min_col, max_row, max_col merged_cell.min_row, merged_cell.min_col, merged_cell.max_row, merged_cell.max_col # 获取左上角值 top_left_value ws.cell(rowmin_row, columnmin_col).value # 填充整个区域 for row in range(min_row, max_row 1): for col in range(min_col, max_col 1): ws.cell(rowrow, columncol, valuetop_left_value) # 取消合并 ws.unmerge_cells(merged_cell.coord) # 保存临时文件并读取 temp_file file_path.replace(.xlsx, _unmerged.xlsx) wb.save(temp_file) return pd.read_excel(temp_file)关键提示此脚本必须在read_excel()前运行且openpyxl版本需 ≥3.1.0旧版unmerge_cells有 bug。5.2 现象Plotly 导出 PNG 时中文显示为方块□□□原因Plotly 默认字体不支持中文kaleido渲染器无法加载系统字体。解决方案1推荐指定plotly.io.kaleido.scope.config.font_family SimHei, Microsoft YaHeiimport plotly.io as pio pio.kaleido.scope.config.font_family SimHei, Microsoft YaHei # 然后调用 fig.write_image(output.png)方案2备用Linux 服务器需安装中文字体并指定路径sudo apt-get install fonts-wqy-zenhei # Ubuntu # 然后在 Python 中 pio.kaleido.scope.config.font_family /usr/share/fonts/truetype/wqy/wqy-zenhei.ttc避坑点font_family必须是字体名称如SimHei不是文件路径Windows 下SimHei有效Mac 下用PingFang SC。5.3 现象Dash 本地运行正常部署到 Ubuntu Nginx 后图表空白Console 报ReferenceError: Plotly is not defined原因Nginx 默认不代理/_dash-component-suites/路径导致 Dash 的 JS 资源 404。解决Nginx 配置追加location /_dash-component-suites/ { alias /var/www/myapp/.dash-component-suites/; expires 1h; add_header Cache-Control public, max-age3600; }关键步骤运行python app.py一次Dash 会自动生成.dash-component-suites/目录将该目录复制到 Nginx 静态文件目录如/var/www/myapp/重启 Nginx。验证访问http://your-domain/_dash-component-suites/应返回 JSON 列表而非 404。5.4 现象px.density_mapbox在部分城市热力图发散看不出聚集效应原因radius参数单位是像素但不同缩本文还有配套的精品资源点击获取