├── .gitignore ├── README.md ├── china_province.geojson ├── data.csv ├── go-choropleth-mapbox.py ├── plotly-choropleth-map.md └── px-choropleth-mapbox.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 使用 plotly 绘制 Choropleth 地图 2 | 3 | 此 repo 主要用于展示如何用 plotly 结合 mapbox 绘制一幅简单的 choropleth 地图,示例使用某日中国的省级 COVID-19 疫情数据,绘制一幅省级 choropleth 地图。 4 | 5 | 更多信息可参见[我的文章](https://blog.csdn.net/u010099080/article/details/104543491)。 6 | 7 | ![go-choropleth-mapbox.gif](https://i.loli.net/2020/02/27/WYHqpbRixzUjI5d.gif) 8 | -------------------------------------------------------------------------------- /data.csv: -------------------------------------------------------------------------------- 1 | ,地区,确诊,疑似,治愈,死亡 2 | 0,上海,335,0,261,3 3 | 1,广西壮族自治区,251,0,112,2 4 | 2,广东,1345,0,774,6 5 | 3,内蒙古自治区,75,0,31,0 6 | 4,安徽,989,0,659,6 7 | 5,江苏,631,0,445,0 8 | 6,宁夏回族自治区,71,0,58,0 9 | 7,吉林,93,0,55,1 10 | 8,辽宁,121,0,80,1 11 | 9,山东,755,0,332,4 12 | 10,四川,527,0,265,3 13 | 11,湖南,1016,0,725,4 14 | 12,云南,174,0,124,2 15 | 13,湖北,64287,0,16738,2495 16 | 14,台湾,28,0,5,1 17 | 15,香港,74,0,12,2 18 | 16,浙江,1205,0,765,1 19 | 17,江西,934,0,645,1 20 | 18,陕西,245,0,162,1 21 | 19,北京,399,0,198,4 22 | 20,河南,1271,0,930,19 23 | 21,福建,293,0,174,1 24 | 22,河北,311,0,221,6 25 | 23,黑龙江,480,0,224,12 26 | 24,海南,168,0,106,5 27 | 25,重庆,575,0,335,6 28 | 26,山西,132,0,88,0 29 | 27,新疆维吾尔自治区,76,0,28,2 30 | 28,甘肃,91,0,78,2 31 | 29,天津,135,0,81,3 32 | 30,贵州,146,0,102,2 33 | 31,西藏自治区,1,0,1,0 34 | 32,青海,18,0,18,0 35 | 33,澳门,10,0,6,0 36 | -------------------------------------------------------------------------------- /go-choropleth-mapbox.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | import plotly.graph_objs as go 3 | import numpy as np 4 | import json 5 | 6 | with open("china_province.geojson", encoding='utf8') as f: 7 | provinces_map = json.load(f) 8 | 9 | df = pd.read_csv('data.csv') 10 | df['确诊_log'] = df.确诊.map(np.log) 11 | fig = go.Figure( 12 | go.Choroplethmapbox( 13 | featureidkey="properties.NL_NAME_1", 14 | geojson=provinces_map, 15 | locations=df.地区, 16 | z=df.确诊_log, 17 | zauto=True, 18 | colorscale='viridis', 19 | reversescale=False, 20 | marker_opacity=0.8, 21 | marker_line_width=0.8, 22 | customdata=np.vstack((df.地区, df.确诊, df.疑似, df.治愈, df.死亡)).T, 23 | hovertemplate="%{customdata[0]}

" 24 | + "确诊:%{customdata[1]}
" 25 | + "疑似:%{customdata[2]}
" 26 | + "治愈:%{customdata[3]}
" 27 | + "死亡:%{customdata[4]}
" 28 | + "", 29 | showscale=True, 30 | ) 31 | ) 32 | fig.update_layout( 33 | mapbox_style="carto-darkmatter", 34 | mapbox_zoom=3, 35 | mapbox_center={"lat": 37.110573, "lon": 106.493924}, 36 | ) 37 | fig.show() 38 | -------------------------------------------------------------------------------- /plotly-choropleth-map.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 使用 plotly 绘制 Choropleth 地图 3 | date: 2020-02-25 16:28 4 | tags: 5 | - Python 6 | - Data Science 7 | --- 8 | 9 | 本文将通过绘制中国省级 Choropleth 地图来解释如何使用 plotly 绘制 Choropleth 地图,主要有两种方法:底层 API `plotly.graph_objects.Choroplethmapbox` 和高层 API `plotly.express.choropleth_mapbox`,数据是 COVID-19 在某一天的疫情数据。 10 | 11 | ## 什么是 Choropleth 地图 12 | 13 | > Choropleth map 即分级统计图。在整个制图区域的若干个小的区划单元内(行政区划或者其他区划单位),根据各分区资料的数量(相对)指标进行分级,并用相应色级或不同疏密的晕线,反映各区现象的集中程度或发展水平的分布差别。—— [Choropleth_百度百科](https://baike.baidu.com/item/Choropleth) 14 | 15 | 简单来说,具体到本文,就是在地图上为每个省上色,根据什么来确定上哪个颜色呢?在本文中就是该省的确诊人数,人数越多,颜色越亮。这样得到的地图就是 Choropleth 地图。 16 | 17 | ## 依赖 18 | 19 | 主要依赖为: 20 | 21 | - [plotly](https://github.com/plotly/plotly.py) 22 | - [pandas](https://github.com/pandas-dev/pandas) 23 | 24 | 均可以通过 `pip` 安装,然后导入: 25 | 26 | ```python 27 | import pandas as pd 28 | import plotly.express as px 29 | import plotly.graph_objs as go 30 | import json 31 | ``` 32 | 33 | ## 数据准备 34 | 35 | - `data.csv`:某日 COVID-19 全国省级疫情数据,用于地图上色 36 | - `china_province.geojson`:中国省级地图 geojson 文件,用于绘制地图轮廓 37 | 38 | 然后导入数据: 39 | 40 | ```python 41 | with open("china_province.geojson") as f: 42 | provinces_map = json.load(f) 43 | df = pd.read_csv('data.csv') 44 | ``` 45 | 46 | ## plotly 的绘图逻辑 47 | 48 | 使用 plotly 绘图,其实就是两点:**data 和 layout**,即数据和布局。其实所有绘图都是这样,只不过在 plotly 里体现得尤为明显,尤其是底层 API。 49 | 50 | data 决定绘图所使用的数据,比如绘制股票折线图用的股票历史数据,绘制疫情地图用的疫情数据。layout 决定图的布局,比如一幅折线图的宽高,一幅地图的风格和中心点。plotly 里一幅图是一个 `Figure` 对象,这个对象就有 `data` 和 `layout` 两个参数。 51 | 52 | ## 方法 1:底层 API `plotly.graph_objects` 53 | 54 | `plotly.graph_objects.Choroplethmapbox`(以下简称 `go.Choroplethmapbox`)是 plotly 的底层 API,其全部参数可参考[其官方文档](https://plot.ly/python/reference/#choroplethmapbox)。不过这参数实在是太多了,下面我通过例子来介绍一下几个常用的。 55 | 56 | 先来看代码: 57 | 58 | ```python 59 | fig = go.Figure( 60 | go.Choroplethmapbox( 61 | geojson=provinces_map, 62 | featureidkey="properties.NL_NAME_1", 63 | locations=df.地区, 64 | z=df.确诊, 65 | zauto=True, 66 | colorscale='viridis', 67 | marker_opacity=0.8, 68 | marker_line_width=0.8, 69 | showscale=True, 70 | ) 71 | ) 72 | fig.update_layout( 73 | mapbox_style="carto-darkmatter", 74 | mapbox_zoom=3, 75 | mapbox_center={"lat": 37.110573, "lon": 106.493924}, 76 | ) 77 | ``` 78 | 79 | 先看下 `go.Choroplethmapbox` 的参数: 80 | 81 | - `geojson`:`dict` 类型,这个就是刚才说的用于绘制地图轮廓的数据,一般从相应的 geojson 文件中用 `json.load` 加载进来。 82 | - `featureidkey`:`str` 类型,默认 为 `id`。函数会使用这个参数和 `locations` 匹配地图单元(比如省份)的名称,以此决定绘制哪些地图单元的轮廓。通常的形式为 `properties.name`,其中的 `name` 需要你自己根据 geojson 文件去指定,比如这里是 `properties.NL_NAME_1`,意思就是 `NL_NAME_1` 这一列是省份名称。这个很重要,设置不正确会导致地图轮廓显示不出来,**一定要保证和 `locations` 中的所有名称保持一致**。 83 | - `locations`: 可以是以下类型:` list,numpy array,数字、字符串或者 datetime 构成的 Pandas series`。指定地图单元名称,决定绘制哪些地图单元的轮廓。同样需要注意**和 `featureidkey` 保持一致**。 84 | - `z`:可以是以下类型:`list,numpy array,数字、字符串或者 datetime 构成的 Pandas series`。指定地图单元对应的数值,函数会将此值映射到 colorscale 中的某一颜色,然后将此颜色涂到相应的地图单元内。通常来说是一个 pandas dataframe 中的某一列,即一个 series。需要注意此参数中值的顺序需要和 `locations` 保持一致,一一对应,如河南在 `locations` 中的索引是 9,那么河南的确诊人数在 `z` 中的索引也必须是 9。 85 | - `zauto`:`bool` 类型,默认为 `True`。是否让颜色自动适应 `z`,即自动计算 `zmin` 和 `zmax`,然后据此来映射 colorscale。 86 | - `colorscale`:通常来说是 `str` 类型,也可以是 [`list` 类型](https://plot.ly/python/colorscales/#custom-discretized-heatmap-color-scale-with-graph-objects)。指定所使用的 colorscale,可使用的值参见[此处](https://plot.ly/python/builtin-colorscales/)。 87 | - `marker_opacity`:`float` 类型,颜色透明度。 88 | - `marker_line_width`:`float` 类型,地图轮廓宽度。 89 | - `showscale`:`bool` 类型。是否显示 colorbar,就是地图旁边的颜色条。 90 | 91 | `fig.update_layout` 的参数同样有很多,主要用来定义布局: 92 | 93 | - `mapbox_style`:`str` 类型,指定 mapbox 风格。可用的 mapbox 风格列表可参见[这里](https://plot.ly/python/mapbox-layers/#base-maps-in-layoutmapboxstyle)。需要注意的是当你使用以下风格之一时,你就需要指定 `mapbox_token`(关于如何获取 token 详细可参见[这里](https://github.com/secsilm/2019-nCoV-dash#%E5%85%B3%E4%BA%8E-mapboxtoken)): 94 | 95 | ```python 96 | ["basic", "streets", "outdoors", "light", "dark", "satellite", "satellite-streets"] 97 | ``` 98 | 99 | - `mapbox_zoom`:`int` 类型,指定地图的缩放级别。 100 | - `mapbox_center`:`dict` 类型,key 为 `lat`(经度)和 `lon`(纬度),指定初始时地图的中心点。 101 | 102 | 最终的效果如图: 103 | 104 | ![go-choropleth-mapbox.gif](https://i.loli.net/2020/02/27/WYHqpbRixzUjI5d.gif) 105 | 106 | ## 方法 2:高层 API `plotly.express.choropleth_mapbox` 107 | 108 | `plotly.express.choropleth_mapbox`(以下简称 `px.choropleth_mapbox`) 是 plotly 的高层 API,严格来说是 [`plotly_express`](https://github.com/plotly/plotly_express) 的接口,但是后来这个包被并入 `plotly`,可以直接用 `plotly.express` 来引入了,这个包主要就是简化了 plotly 的绘图方法。 109 | 110 | 详细参数可参考其[官方文档](https://plot.ly/python-api-reference/generated/plotly.express.choropleth_mapbox.html#plotly.express.choropleth_mapbox)。其实大部分参数是异曲同工的,下面我同样使用相同的数据来绘制地图,解释下。 111 | 112 | 老规矩,先来看代码: 113 | 114 | ```python 115 | fig = px.choropleth_mapbox( 116 | data_frame=df, 117 | geojson=provinces_map, 118 | color='确诊', 119 | locations="地区", 120 | featureidkey="properties.NL_NAME_1", 121 | mapbox_style="carto-darkmatter", 122 | color_continuous_scale='viridis', 123 | center={"lat": 37.110573, "lon": 106.493924}, 124 | zoom=3, 125 | ) 126 | ``` 127 | 128 | - `data_frame`:通常来说是 `pd.DataFrame` 格式。我们需要把绘图用到的数据都放到这个参数里面,后续很多参数都是基于此的,具体来说就是其中的列名。在 plot express 的各个绘图方法中,`DataFrame` 其实是最为方便的格式,也是官方推荐的格式,官方的大部分示例都是使用的这个格式。 129 | - `geojson`:和 `go.Choroplethmapbox` 的同名参数对应。 130 | - `color`:通常为 `str` 类型,`data_frame` 的列名。和 `go.Choroplethmapbox` 中的 `z` 对应。 131 | - `locations`:通常为 `str` 类型,`data_frame` 的列名。和 `go.Choroplethmapbox` 中的同名参数对应。 132 | - `featureidkey`:和 `go.Choroplethmapbox` 的同名参数对应。 133 | - `mapbox_style`:和 `update_layout` 的同名参数对应。 134 | - `color_continuous_scale`:和 `go.Choroplethmapbox` 中的 `colorscale` 对应。 135 | - `center`:和 `update_layout` 中的 `mapbox_center` 对应。 136 | - `zoom`:和 `update_layout` 中的 `mapbox_zoom` 对应。 137 | 138 | 最终的效果如图: 139 | 140 | ![这里记的放图](https://i.imgur.com/POkHX4t.gif) 141 | 142 | ## 一些没说到的 143 | 144 | 为了阅读体验,本文没有解释更多的参数,但我相信这已经能让你绘制一幅不错的 choropleth 地图了。有时间我会继续写一写如何在 dash 中融入这些地图,并实时更新。 145 | 146 | 其实本文所讲的是地图是一种 tile map,和这种地图对应的是一种轮廓地图,没有 mapbox 这种底图,只绘制 geojson 文件中定义的轮廓,如下面这幅图: 147 | 148 | ![中国地图](https://i.loli.net/2020/02/27/a6K4Sbj8IYzqrA1.png) 149 | 150 | plotly 也可以绘制这种地图,只需要去掉本文所讲的函数中 `mapbox` 即可:`go.Choropleth` 和 `px.choropleth`,感兴趣可以参考[这里](https://plot.ly/python/choropleth-maps/)的示例。 151 | 152 | ## Reference 153 | 154 | - [Mapbox Choropleth Maps | Python | Plotly](https://plot.ly/python/mapbox-county-choropleth/) 155 | - [Choropleth Maps | Python | Plotly](https://plot.ly/python/choropleth-maps/#base-map-configuration) 156 | 157 | ## END -------------------------------------------------------------------------------- /px-choropleth-mapbox.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | import plotly.express as px 3 | import numpy as np 4 | import json 5 | 6 | with open("china_province.geojson", encoding='utf8') as f: 7 | provinces_map = json.load(f) 8 | 9 | df = pd.read_csv('data.csv') 10 | df.确诊 = df.确诊.map(np.log) 11 | 12 | fig = px.choropleth_mapbox( 13 | df, 14 | geojson=provinces_map, 15 | # color=f"{selected_radio}区间", 16 | color='确诊', 17 | locations="地区", 18 | featureidkey="properties.NL_NAME_1", 19 | mapbox_style="carto-darkmatter", 20 | # color_discrete_map={ 21 | # "0": colorscales[selected_radio][0], 22 | # "1-9": colorscales[selected_radio][1], 23 | # "10-99": colorscales[selected_radio][2], 24 | # "100-499": colorscales[selected_radio][3], 25 | # "500-999": colorscales[selected_radio][4], 26 | # "1000-9999": colorscales[selected_radio][5], 27 | # "10000+": colorscales[selected_radio][6], 28 | # }, 29 | # category_orders={f"{selected_radio}区间": labels}, 30 | color_continuous_scale='viridis', 31 | center={"lat": 37.110573, "lon": 106.493924}, 32 | zoom=3, 33 | # hover_name="地区", 34 | # hover_data=["确诊", "疑似", "治愈", "死亡"], 35 | ) 36 | # fig.update_layout(margin={"r": 0, "t": 0, "l": 0, "b": 0}) 37 | fig.show() 38 | --------------------------------------------------------------------------------