├── .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 | 
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 | 
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 | 
141 |
142 | ## 一些没说到的
143 |
144 | 为了阅读体验,本文没有解释更多的参数,但我相信这已经能让你绘制一幅不错的 choropleth 地图了。有时间我会继续写一写如何在 dash 中融入这些地图,并实时更新。
145 |
146 | 其实本文所讲的是地图是一种 tile map,和这种地图对应的是一种轮廓地图,没有 mapbox 这种底图,只绘制 geojson 文件中定义的轮廓,如下面这幅图:
147 |
148 | 
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 |
--------------------------------------------------------------------------------