Read Medium logo
No Results
翻译为
Read Medium Logo
Free OpenAI o1 chatTry OpenAI o1 API
Read Medium logo
No Results
翻译为
avatarKamol C. Roy

总结

本文介绍了如何使用 Python 库 Manim 创建动态地理空间可视化,并以 2017 年美国人口密度为例,演示了如何通过 Manim 制作纵横图动画。

摘要

文章作者在寻找理解神经网络的视频内容时,被 Grant Sanderson 的 3Blue1Brown 频道上使用 Manim 制作的视频震撼了,特别是其对复杂理论和数学的清晰视觉表达。Manim 是一个由 Sanderson 开发的 Python 库,用于创建高质量的数学动画,它的功能不仅限于数学领域,还可以用于一般的几何可视化和地理空间数据可视化。

作者首先介绍了需要安装的两个主要软件包:Geopandas 和 Manim,并提供了相应的安装文档链接。接着,作者以 2017 年美国各州人口数据为例,展示了如何使用 Geopandas 读取数据并使用 Matplotlib 进行静态地图的可视化。

文章继续详细说明了使用 Manim 创建动态地理空间可视化的四个主要步骤:将 GIS 几何数据转换为 Manim 对象;设置坐标轴并应用适当的比例;根据属性应用颜色;以及自定义动画。作者通过示例代码展示了如何将美国地图边界转换为 Manim 对象,并创建了一个名为“Animate_population_density.mp4”的动画。

最后,作者提供了完整的代码,用于生成美国各州人口密度的纵横图动画,并鼓励读者使用 Manim 进行空间可视化。作者还提供了一个类似的可视化链接,展示了更多定制功能的示例。

观点

  • Manim 是一个强大的 Python 库,适合创建数学和地理空间数据的动画可视化。
  • 通过将 GIS 数据转换为 Manim 对象,可以创建动态的地理空间可视化,如纵横图。
  • 动态可视化比静态可视化更能吸引观众,提供更丰富的信息表达。
  • 使用 Manim 制作动画需要对 Python 编程和 GIS 数据处理有一定的了解。
  • 作者通过实际的编程示例和动画效果,展示了 Manim 在地理空间可视化领域的应用潜力。
  • 鼓励读者尝试使用 Manim 进行空间数据可视化,并通过代码修改来实现更多定制效果。

Create Dynamic Geo-Spatial Visualization using Manim
使用 Manim 创建动态地理空间可视化

I was looking for video content to better understand neural networks. Then, when I came across this video by Grant Sanderson of 3Blue1Brown I was blown away by the visual clarity of such a complex theory and math. Most of the videos of the 3Blue1Brown channel are created using Manim (developed by Grant Sanderson), a powerful Python library that brings math to life using animation. It’s designed for creating high-quality mathematical animations, but its capabilities extend to general geometric visualization, making it perfect for visualizing geospatial data. In this article, I will introduce a way to use this powerful library for spatial visualization (choropleth).
我一直在寻找视频内容,以便更好地理解神经网络。当我看到 3Blue1Brown 的格兰特-桑德森(Grant Sanderson)制作的这段视频时,我被如此复杂的理论和数学的清晰视觉效果所震撼。3Blue1Brown 频道的大部分视频都是使用 Manim(由 Grant Sanderson 开发)制作的,这是一个功能强大的 Python 库,能通过动画将数学变得栩栩如生。它专为创建高质量的数学动画而设计,但其功能可扩展到一般的几何可视化,使其成为地理空间数据可视化的完美工具。在本文中,我将介绍如何将这个强大的库用于空间可视化(choropleth)。

Before starting, we will need two main packages to install. Below are the package names and the installation documents:
开始之前,我们需要安装两个主要软件包。以下是软件包名称和安装文件:

  • Geopandas (Getting Started — GeoPandas 0+untagged.50.g5558c35.dirty documentation)
    Geopandas(入门 - GeoPandas 0+untagged.50.g5558c35.dirty文档)
  • Manim (Installation — Manim Community v0.18.0)
    Manim (安装 - Manim Community v0.18.0)

Now, let’s collect some data to visualize, I will use 2017 US population data data by states as an example, but you can use any data.
现在,让我们收集一些数据来进行可视化,我将以 2017 年美国各州人口数据为例,但你也可以使用任何数据。

import geopandas as gpd
import matplotlib.pyplot as plt
from manim import *

us_population_2017 = gpd.read_file("https://services.arcgis.com/P3ePLMYs2RVChkJx/arcgis/rest/services/USA_States_Generalized/FeatureServer/0/query?outFields=*&where=1%3D1&f=geojson")
us_population_2017.head()

Let’s visualize the data using Matplotlib. Of course animation can be made using the matplotlib library as well but let’s discuss this in a different article.
让我们使用 Matplotlib 将数据可视化。当然,也可以使用 matplotlib 库制作动画,但我们将在另一篇文章中讨论这个问题。

import matplotlib.pyplot as plt

fig, ax = plt.subplots(figsize=(10, 7))
cax = fig.add_axes([0.93, 0.2, 0.02, 0.6])
us_population_2017.plot(ax=ax, 
                        column='POP_SQMI', 
                        legend=True, 
                        cax=cax,
                        vmax=500,
                        legend_kwds = {'label': 'per sqmi'}
                       )

custom_ticklabels = []
for i in cax.get_yticklabels():
    ticklabel = i.get_text()
    if ticklabel=='500':
        custom_ticklabels.append(f'>{ticklabel}')
    else:
        custom_ticklabels.append(ticklabel)
        
cax.set_yticklabels(custom_ticklabels)

ax.set_title('Population per Square Mile')
plt.show()

here’s what the static maps look like
下面是静态地图的样子

2017 US Population Density Per Square Mile
2017 年美国每平方英里人口密度

The steps of creating manim animation from geospatial dataset can be divided into 3 major steps: 1) Converting GIS geometric data to Manim Object 2) Setting up an Axis and applying appropriate scale 3) Applying color based on attributes, 4) Customizing the animation.
从地理空间数据集创建 Manim 动画的步骤可分为 3 个主要步骤:1) 将 GIS 几何数据转换为 Manim 对象;2) 设置轴线并应用适当的比例;3) 根据属性应用颜色;4) 自定义动画。

Converting GIS geometric data to Manim Object: To convert polygon GIS geometry to Manim line and polygon objects for a choropleth visualization, follow these steps:
将 GIS 几何数据转换为 Manim 对象:要将多边形 GIS 几何数据转换为 Manim 线条和多边形对象,以实现纵横图可视化,请按照以下步骤操作:

  • Extract Coordinates: Extract the vertex coordinates of the polygons from the GIS dataset. These coordinates define the boundaries of each polygon.
    提取坐标:从 GIS 数据集中提取多边形的顶点坐标。这些坐标定义了每个多边形的边界。
  • Create Line and Polygon Objects: Use the extracted coordinates to create Manim line and polygon objects. Each polygon corresponds to a closed shape defined by its vertices. Additionally, create line objects to represent the boundaries of the polygons if necessary.
    创建直线和多边形对象使用提取的坐标创建马尼姆直线和多边形对象。每个多边形对应一个由其顶点定义的封闭形状。此外,如有必要,还可创建线条对象来表示多边形的边界。

Setting up an Axis and applying the appropriate scale: Determine the coordinate system you want to use for your visualization. This could be Cartesian coordinates, polar coordinates, or any other geometric projection suitable for your data. We can do this by creating Axes Objects: Use Manim’s Axes class to create axes objects based on your chosen coordinate system. Specify parameters such as the range, labels, and tick marks for each axis
设置坐标轴并应用适当的比例:确定要用于可视化的坐标系。这可以是笛卡尔坐标系、极坐标系或任何其他适合数据的几何投影。我们可以通过创建轴对象来实现这一点:使用 Manim 的轴类来创建基于所选坐标系的轴对象。为每个轴指定范围、标签和刻度线等参数

Below is an example using USA boundaries. For simplicity, I will exclude Alaska and Hawaii.
下面是一个使用美国边界的示例。为简单起见,我将阿拉斯加和夏威夷排除在外。

%%manim -qm -v WARNING Animate_population_density
# the above magic command is needed for running Manim in jupyter notebook

world = gpd.read_file(gpd.datasets.get_path('naturalearth_lowres'))

exploded_usa = world[world['iso_a3']=='USA'].explode() # to convert multipolygon to polygon
exploded_usa['part'] = [f'part_{i}' for i in range(len(exploded_usa))]
# exploded_usa.explore(column='part')

polygon = exploded_usa[exploded_usa['part']=='part_0'].geometry.values[0] # excluding Alaska and Hawai

#extract the coordinates
polygon_border_xy = np.array(polygon.boundary.coords)

class Animate_population_density(Scene):
   def construct(self):
        # set up Manim Axis
        axes = Axes(
            x_range=[min(polygon_border_xy[:,0])-2.5, max(polygon_border_xy[:,0])+2.5],
            y_range=[min(polygon_border_xy[:,1]), max(polygon_border_xy[:,1])],
            axis_config={"color": BLUE},
        )
        
        boundary_line = axes.plot_line_graph(polygon_border_xy[:,0], polygon_border_xy[:,1], 
                                       add_vertex_dots=False, 
                                       line_color=WHITE,
                                       stroke_width=5 )
#         bd_line.move_to(LEFT*3)
        self.play( Create(boundary_line, run_time=2))
        self.wait(1)

Running this code, we create the following animation named “Animate_population_density.mp4” under “media/videos” directory.
运行这段代码后,我们在 "media/videos "目录下创建了名为 "Animate_population_density.mp4 "的动画。

Here is the full code to generate population density Choropleth animation for the States.
以下是为各州生成人口密度 Choropleth 动画的完整代码。

#Full code 
import geopandas as gpd
import matplotlib.pyplot as plt
from manim import *
from colormap import rgb2hex

us_population_2017 = gpd.read_file("https://services.arcgis.com/P3ePLMYs2RVChkJx/arcgis/rest/services/USA_States_Generalized/FeatureServer/0/query?outFields=*&where=1%3D1&f=geojson")
relevant_columns = ['STATE_NAME', 'POPULATION', 'POP_SQMI', 'geometry']
# us_population_2017[relevant_columns].head()

us_population_2017 = us_population_2017.explode()

us_population_2017 = us_population_2017[~us_population_2017['STATE_NAME'].isin(['Alaska', 'Hawaii'])]

us_population_2017 = us_population_2017.sort_values(by='POP_SQMI', ascending=False)

cmap = plt.get_cmap('viridis')
norm = plt.Normalize(us_population_2017['POP_SQMI'].min(), 500)


def get_line_coord(linestring):

    border_xy = np.array(linestring.boundary.coords)
    
    return border_xy



def get_line_and_area(axes, border_xy, pop):

    color = cmap(norm(pop))
    hex_color = rgb2hex(*color)

    line = axes.plot_line_graph(border_xy[:,0], border_xy[:,1], 
                                add_vertex_dots=False,
                                line_color=BLACK, 
                                stroke_width=1 )
    points = axes.coords_to_point(border_xy)
    area = Polygon(*points,fill_opacity=0.5, color=hex_color, stroke_width=1)
    dist_center = area.get_center()

    # line.to_edge(UR)
    # area.to_edge(UR)

    return line, area, dist_center


world = gpd.read_file(gpd.datasets.get_path('naturalearth_lowres'))

exploded_usa = world[world['iso_a3']=='USA'].explode() # to convert multipolygon to polygon
exploded_usa['part'] = [f'part_{i}' for i in range(len(exploded_usa))]
# exploded_usa.explore(column='part')

polygon = exploded_usa[exploded_usa['part']=='part_0'].geometry.values[0] # excluding Alaska and Hawai

#extract the coordinates
polygon_border_xy = np.array(polygon.boundary.coords)

class Animate_population_density(Scene):
   def construct(self):
    
        self.camera.background_color = WHITE
        # set up Manim Axis
        axes = Axes(
            x_range=[min(polygon_border_xy[:,0])-2.5, max(polygon_border_xy[:,0])+2.5],
            y_range=[min(polygon_border_xy[:,1]), max(polygon_border_xy[:,1])],
            axis_config={"color": BLUE},
        )
        
        boundary_line = axes.plot_line_graph(polygon_border_xy[:,0], polygon_border_xy[:,1], 
                                       add_vertex_dots=False, 
                                       line_color=BLACK,
                                       stroke_width=5 )
#         bd_line.move_to(LEFT*3)
        self.play( Create(boundary_line, run_time=1))
        self.wait(0.5)

        
        
        for state in us_population_2017['STATE_NAME'].unique():
            state_df = us_population_2017[us_population_2017['STATE_NAME']==state]
            
            if len(state_df)>1:
                
                states_line_group = VGroup()
                state_area_group = VGroup()
                
                for row in state_df.itertuples():
                
                    geometry = row.geometry
                    density = row.POP_SQMI

                    state_line, state_area, center = get_line_and_area(axes, 
                                                                       get_line_coord(geometry), 
                                                                       density
                                                                      )

                    states_line_group.add(state_line)
                    state_area_group.add(state_area)





                state_label = Text(f"{state}: {density:.2f} per square mile ",
                                   font_size=25, color=BLACK)

                state_label.move_to(UP + 2.3 * UP)

                self.play( Create(states_line_group) , Write(state_label),  run_time=2)

                self.play( Create(state_area_group), run_time=2)

                self.play(FadeOut(state_label))
               
                
            else:
                geometry = state_df.geometry.values[0]
                density = state_df.POP_SQMI.values[0]

                state_line, state_area, center = get_line_and_area(axes, 
                                                                   get_line_coord(geometry), 
                                                                   density
                                                                  )

                
                state_label = Text(f"{state}: {density:.2f} per square mile ",
                                   font_size=25, color=BLACK)
                
                state_label.move_to(UP + 2.3 * UP)
                
                self.play( Create(state_line) , Write(state_label),  run_time=2)
                
                self.play( Create(state_area), run_time=2)
                
                self.play(FadeOut(state_label))

This code will generate this video:
这段代码将生成这段视频:

Another way to animate this can be animating all states together like this ( I challenge you to modify the code yourself to get this effect :) ).
另一种动画制作方法是将所有状态一起做成这样的动画(我挑战你自己修改代码来获得这种效果:)。

Here is a link of a similar visualization with some more customizations:
下面是一个类似的可视化链接,其中包含更多定制功能:

Thank you for taking the time to read my first article on Medium. Your support means a lot to me as I embark on this new writing journey. I’m eager to learn and grow with your feedback, so please feel free to leave your thoughts and comments below. I hope I can motivate you to use Manim for spatial visualization.
感谢您花时间阅读我在 Medium 上发表的第一篇文章。在我踏上这段新的写作旅程时,你们的支持对我意义重大。我渴望在您的反馈中学习和成长,因此请随时在下面留下您的想法和评论。希望我能激励你使用 Manim 进行空间可视化。

Visualization
Manim
Geospatial
Python
Data Science
Recommended from ReadMedium
avatarEran Feit
3D Photo Magic | Convert Any Picture to 3D with Python

Hi,

2 min read
avatarJustin Morgan Williams
Spatial Machine Learning

An intro in applying machine learning techniques to spatial data with R

14 min read
avatarAmit Yadav
Mastering Spatial Data Analysis with Python

Are You Feeling Overwhelmed Learning Data Science?

12 min read
avatarAlexzap
25 Staggering Use-Case Examples of Geospatial Data Visualization & Analytics with Python

Unveiling the Great Value of Geo 3D Patterns & Trends — USA & World

31 min read
avatarDr. Ananth G S
Optimizing an OCR accuracy with Pytesseract — config options (pre-process)

This article discusses configuration options that help an OCR engine easily identify and recognize text in images.

3 min read
avatarAmanda Iglesias Moreno
5 Amazing Plotly Visualizations You Didn’t Know You Could Create

Explore Waffle Charts, Calendar Plots, Hexagon Maps, Parliament Diagrams, and Bump Charts for Advanced Data Visualizations in Plotly

12 min read