You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用Streamlit与Plotly Express绘制印尼GeoJSON地理数据?

印尼城市/省份GeoJSON地图在Streamlit+Plotly中显示为空的解决方法

问题描述

尝试用Streamlit和Plotly Express绘制印尼城市与省份的GeoJSON数据,数据来自DuckDB数据库,绘图前会与额外数据合并。转换为GeoJSON后地图能正常显示,但无任何数据渲染。

已尝试的操作

  • 检查GeoJSON与DataFrame的索引是否匹配
  • 在px.choropleth_mapbox中使用featureidkey参数
  • 验证索引与GeoJSON id的数据类型是否匹配

相关代码片段

class InteractiveMap:
    def __call__(self):
        return self.interactive_map()

    def interactive_map(self):
        st.title('Interactive Map of Indonesia')

        choice = st.selectbox("Choose between Cities and Provinces", ["Cities", "Provinces"])

        conn = duckdb.connect('oeroenremboog.db')
        cursor = conn.cursor()

        if choice == "Cities":
            cursor.execute("SELECT * FROM indonesia_cities;")
        else:
            cursor.execute("SELECT * FROM indonesia_provinces;")

        columns = [desc[0] for desc in cursor.description]
        indonesia_map = pd.DataFrame(cursor.fetchall(), columns=columns)

        if 'geometry' in indonesia_map.columns:
            indonesia_map['geometry'] = indonesia_map['geometry'].apply(wkb.loads, hex=True)

        indonesia_map = gpd.GeoDataFrame(indonesia_map, geometry='geometry')

        # Load the JSON data
        with open('src/data/diff_percentage_dm1_dm2.json') as f:
            diff_data = json.load(f)
        diff_df = pd.DataFrame(diff_data)

        name_column = 'Name' if choice == "Cities" else 'Propinsi'

        if name_column in indonesia_map.columns and 'kabupaten_tinggal' in diff_df.columns:
            merged_data = pd.merge(indonesia_map, diff_df, left_on=name_column, right_on='kabupaten_tinggal', how='left')
        else:
            st.error(f"Missing column {name_column} in either of the DataFrames.")
            return

        # Convert the 'diff_percentage' column to numeric
        merged_data['diff_percentage'] = pd.to_numeric(merged_data['diff_percentage'], errors='coerce')
        merged_data['diff_percentage'].fillna(0, inplace=True)

        indonesia_map.crs = "EPSG:4326"


        # Convert GeoDataFrame to GeoJSON
        geojson_data = json.loads(merged_data.to_json())

        # Debugging
        st.write(f"Sample GeoJSON: {str(geojson_data)[:500]}")
        st.write(f"Sample merged_data: {str(merged_data)[:500]}")
        
        # Plotting
        fig = px.choropleth_mapbox(merged_data, 
                                   geojson=geojson_data, 
                                   locations=merged_data.index,  # DataFrame index
                                   color='diff_percentage',
                                   color_continuous_scale="Viridis",
                                   range_color=(-100, 100),
                                   mapbox_style="carto-positron",
                                   opacity=0.5, 
                                   labels={'diff_percentage':'Difference Percentage'},
                                   center={"lat": -2, "lon": 118},
                                   zoom=3.4,
                                   featureidkey="properties.id")

        st.plotly_chart(fig)

        cursor.close()

        return merged_data

调试信息样本

indonesia_map前10行数据

{'Name': {0: 'SIMEULUE', 1: 'ACEH SINGKIL', 2: 'ACEH SELATAN', 3: 'ACEH TENGGARA', 4: 'ACEH TIMUR', 5: 'ACEH TENGAH', 6: 'ACEH BARAT', 7: 'ACEH BESAR', 8: 'PIDIE', 9: 'BIREUEN'}, 'latitude': {0: 2.613334894180298, 1: 2.349949598312378, 2: 3.1632587909698486, 3: 3.369655132293701, 4: 4.628895282745361, 5: 4.530141830444336, 6: 4.456692218780518, 7: 5.3799920082092285, 8: 5.068343639373779, 9: 5.093278884887695}, 'longitude': {0: 96.08564758300781, 1: 97.84710693359375, 2: 97.43519592285156, 3: 97.69552612304688, 4: 97.62864685058594, 5: 96.85894012451172, 6: 96.18546295166016, 7: 95.51558685302734, 8: 96.00715637207031, 9: 96.60938262939453}, 'geometry': {0: <POINT (96.086 2.613)>, 1: <POINT (97.847 2.35)>, 2: <POINT (97.435 3.163)>, 3: <POINT (97.696 3.37)>, 4: <POINT (97.629 4.629)>, 5: <POINT (96.859 4.53)>, 6: <POINT (96.185 4.457)>, 7: <POINT (95.516 5.38)>, 8: <POINT (96.007 5.068)>, 9: <POINT (96.609 5.093)>}}

merged_data前10行数据

{'Name': {0: 'SIMEULUE', 1: 'ACEH SINGKIL', 2: 'ACEH SELATAN', 3: 'ACEH TENGGARA', 4: 'ACEH TIMUR', 5: 'ACEH TENGAH', 6: 'ACEH BARAT', 7: 'ACEH BESAR', 8: 'PIDIE', 9: 'BIREUEN'}, 'latitude': {0: 2.613334894180298, 1: 2.349949598312378, 2: 3.1632587909698486, 3: 3.369655132293701, 4: 4.628895282745361, 5: 4.530141830444336, 6: 4.456692218780518, 7: 5.3799920082092285, 8: 5.068343639373779, 9: 5.093278884887695}, 'longitude': {0: 96.08564758300781, 1: 97.84710693359375, 2: 97.43519592285156, 3: 97.69552612304688, 4: 97.62864685058594, 5: 96.85894012451172, 6: 96.18546295166016, 7: 95.51558685302734, 8: 96.00715637207031, 9: 96.60938262939453}, 'geometry': {0: <POINT (96.086 2.613)>, 1: <POINT (97.847 2.35)>, 2: <POINT (97.435 3.163)>, 3: <POINT (97.696 3.37)>, 4: <POINT (97.629 4.629)>, 5: <POINT (96.859 4.53)>, 6: <POINT (96.185 4.457)>, 7: <POINT (95.516 5.38)>, 8: <POINT (96.007 5.068)>, 9: <POINT (96.609 5.093)>}, 'kabupaten_tinggal': {0: 'SIMEULUE', 1: 'ACEH SINGKIL', 2: 'ACEH SELATAN', 3: 'ACEH TENGGARA', 4: 'ACEH TIMUR', 5: 'ACEH TENGAH', 6: 'ACEH BARAT', 7: 'ACEH BESAR', 8: 'PIDIE', 9: 'BIREUEN'}, 'total_dm_tipe_i': {0: '236', 1: '165', 2: '464', 3: '194', 4: '117', 5: '231', 6: '167', 7: '1216', 8: '221', 9: '200'}, 'total_dm_tipe_ii': {0: '180', 1: '321', 2: '1076', 3: '256', 4: '447', 5: '343', 6: '916', 7: '1788', 8: '731', 9: '706'}, 'diff_percentage': {0: 0.1346153846153846, 1: -0.3209876543209876, 2: -0.3974025974025974, 3: -0.1377777777777777, 4: -0.5851063829787234, 5: -0.1951219512195122, 6: -0.6915974145891042, 7: -0.1904127829560586, 8: -0.5357142857142857, 9: -0.5584988962472405}}

GeoJSON前500字符

{'type': 'FeatureCollection', 'features': [{'id': '0', 'type': 'Feature', 'properties': {'Name': 'SIMEULUE', 'latitude': 2.613334894180298, 'longitude': 96.08564758300781, 'kabupaten_tinggal': 'SIMEULUE', 'total_dm_tipe_i': '236', 'total_dm_tipe_ii': '180', 'diff_percentage': 0.1346153846153846}, 'geometry': {'type': 'Point', 'coordinates': [96.08564793225175, 2.6133349583186596]}}, {'id': '1', 'type': 'Feature', 'properties': {'Name': 'ACEH SINGKIL', 'latitude': 2.349949598312378, 'longitude':

问题分析与解决方案

核心问题

你使用了px.choropleth_mapbox来绘制点要素,但该函数是专门用于渲染**面状区域(如省份、城市边界)**的填充地图,无法识别点几何数据,因此导致数据无法渲染。

修正步骤

  1. 替换绘图函数:将px.choropleth_mapbox改为px.scatter_mapbox,该函数支持点数据的可视化,可根据数值设置点的颜色、大小等属性。
  2. 简化参数:散点图不需要geojson、locations、featureidkey这些参数,直接使用数据中的纬度和经度列即可。
  3. 修正CRS设置:将CRS设置应用到最终的merged_data,而非原始的indonesia_map。

修改后的绘图代码片段

# 修正CRS设置到最终数据集
merged_data.crs = "EPSG:4326"

# 调试信息保留
st.write(f"Sample merged_data: {str(merged_data)[:500]}")

# 替换为散点图绘制
fig = px.scatter_mapbox(merged_data,
                        lat='latitude',
                        lon='longitude',
                        color='diff_percentage',
                        color_continuous_scale="Viridis",
                        range_color=(-100, 100),
                        mapbox_style="carto-positron",
                        opacity=0.8,
                        size_max=15,  # 可根据需求调整点的最大尺寸
                        labels={'diff_percentage':'Difference Percentage'},
                        center={"lat": -2, "lon": 118},
                        zoom=3.4,
                        hover_data=['Name', 'diff_percentage'])  # 鼠标悬停显示额外信息

st.plotly_chart(fig)

额外说明

  • 如果你的目标是绘制区域填充地图,需要确保GeoDataFrame中的geometry字段是多边形(Polygon/MultiPolygon)类型,而非点(Point)。
  • 若坚持使用choropleth_mapbox,需确认GeoJSON中的要素是面状,且featureidkey参数需指向GeoJSON要素的顶级id(即featureidkey="id",而非properties.id),同时locations参数需匹配DataFrame中对应的值。

内容的提问来源于stack exchange,提问作者ebuzz168

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.11 11:24:49