Python數據可視化:5段代碼搞定散點圖繪製與使用,值得收藏
- 2019 年 12 月 19 日
- 筆記

導讀:什麼是散點圖?可以用來呈現哪些數據關係?在數據分析過程中可以解決哪些問題?怎樣用Python繪製散點圖?本文逐一為你解答。
作者:屈希峰
來源:大數據DT(ID:bigdatadt)

01 概述
散點圖(Scatter)又稱散點分佈圖,是以一個變量為橫坐標,另一個變量為縱坐標,利用散點(坐標點)的分佈形態反映變量統計關係的一種圖形。
特點是能直觀表現出影響因素和預測對象之間的總體關係趨勢。優點是能通過直觀醒目的圖形方式反映變量間關係的變化形態,以便決定用何種數學表達方式來模擬變量之間的關係。散點圖不僅可傳遞變量間關係類型的信息,還能反映變量間關係的明確程度。
通過觀察散點圖數據點的分佈情況,我們可以推斷出變量間的相關性。如果變量之間不存在相互關係,那麼在散點圖上就會表現為隨機分佈的離散的點,如果存在某種相關性,那麼大部分的數據點就會相對密集並以某種趨勢呈現。
數據的相關關係大體上可以分為:正相關(兩個變量值同時增長)、負相關(一個變量值增加,另一個變量值下降)、不相關、線性相關、指數相關等,表現在散點圖上的大致分佈如圖1所示。那些離點集群較遠的點我們稱之為離群點或者異常點。

▲圖1 散點數據的相關性
在Python體系中,可使用Scipy、Statsmodels或Sklearn等對離散點進行回歸分析,歸納現有數據並進行預測分析。對於那些變量之間存在密切關係,但是這些關係又不像數學公式和物理公式那樣能夠精確表達的,散點圖是一種很好的圖形工具,可以進行直觀展示,如圖2所示。

▲圖2 散點數據擬合(線性)
但是在分析過程中需要注意,變量之間的相關性並不等同於確定的因果關係,仍需要考慮其他影響因素。
02 實例
散點圖代碼示例如下所示。
代碼示例①
# 數據 x = [1, 2, 3, 4, 5] y = [6, 7, 2, 4, 5] # 畫布:尺寸 p = figure(plot_width=400, plot_height=400) # 畫圖 p.scatter(x, y, size=20, # screen units 顯示器像素單位 # radius=1, # data-space units 坐標軸單位 marker="circle", color="navy", alpha=0.5) # p.circle(x, y, size=20, color="navy", alpha=0.5) # 顯示 show(p)
運行結果如圖3所示。

▲圖3 代碼示例①運行結果
代碼示例①中第7行使用scatter方法進行散點圖繪製;第11行採用circle方法進行散點圖繪製(推薦)。關於這兩個方法的參數說明如下。
p.circle(x, y, **kwargs)參數說明。
- x (str or seq[float]) : 離散點的x坐標,列名或列表
- y (str or seq[float]) : 離散點的y坐標
- size (str or list[float]) : 離散點的大小,屏幕像素單位
- marker (str, or list[str]) : 離散點標記類型名稱或名稱列表
- color (color value, optional) : 填充及輪廓線的顏色
- source (`~bokeh.models.sources.ColumnDataSource`) : Bokeh專屬數據格式
- **kwargs: 其他自定義屬性;其中標記點類型marker默認值為:「marker="circle"」,可以用「radius」定義圓的半徑大小(單位為坐標軸單位)。這在Web數據化中非常有用,不同的方式,在不同的設備上的展示效果會有些許差異。
p.scatter(x, y, **kwargs)參數說明。
- x (:class:`~bokeh.core.properties.NumberSpec` ) : x坐標
- y (:class:`~bokeh.core.properties.NumberSpec` ) : y坐標
- angle (:class:`~bokeh.core.properties.AngleSpec` ) : 旋轉角度
- angle_units (:class:`~bokeh.core.enums.AngleUnits`) : (default: 'rad') 默認:弧度,也可以採用度('degree')
- fill_alpha (:class:`~bokeh.core.properties.NumberSpec` ) : (default: 1.0) 填充透明度,默認:不透明
- fill_color (:class:`~bokeh.core.properties.ColorSpec` ) : (default: 'gray') 填充顏色,默認:灰色
- line_alpha (:class:`~bokeh.core.properties.NumberSpec` ) : (default: 1.0) 輪廓線透明度,默認:不透明
- line_cap : (:class:`~bokeh.core.enums.LineCap` ) : (default: 'butt') 線端(帽)
- line_color (:class:`~bokeh.core.properties.ColorSpec` ) : (default: 'black') 輪廓線顏色,默認:黑色
- line_dash (:class:`~bokeh.core.properties.DashPattern` ) : (default: []) 虛線
- line_dash_offset (:class:`~bokeh.core.properties.Int` ) : (default: 0) 虛線偏移
- line_join (:class:`~bokeh.core.enums.LineJoin` ) : (default: 'bevel')
- line_width (:class:`~bokeh.core.properties.NumberSpec` ) : (default: 1) 線寬,默認:1
另外,Bokeh中的一些屬性,如`~bokeh.core.properties.NumberSpec `、`~bokeh.core.properties.ColorSpec`可以在Jupyter notebook中通過`import bokeh.core.properties.NumberSpec `導入該屬性,然後再查看其詳細的使用說明。
代碼示例②
# 數據 N = 4000 x = np.random.random(size=N) * 100 # 隨機點x坐標 y = np.random.random(size=N) * 100 # 隨機點y坐標 radii = np.random.random(size=N) * 1.5 # 隨機半徑 # 工具條 TOOLS="hover,crosshair,pan,wheel_zoom,box_zoom,reset,tap,save,box_select,poly_select,lasso_select" # RGB顏色,畫布1,繪圖1 colors2 = ["#%02x%02x%02x" % (int(r), int(g), 150) for r, g in zip(50+2*x, 30+2*y)] p1 = figure(width=300, height=300, tools=TOOLS) p1.scatter(x,y, radius=radii, fill_color=colors2, fill_alpha=0.6, line_color=None) # RGB顏色,畫布2,繪圖2 colors2 = ["#%02x%02x%02x" % (150, int(g), int(b)) for g, b in zip(50+2*x, 30+2*y)] p2 = figure(width=300, height=300, tools=TOOLS) p2.scatter(x,y, radius=radii, fill_color=colors2, fill_alpha=0.6, line_color=None) # 直接顯示 # show(p1) # show(p2) # 網格顯示 from bokeh.layouts import gridplot grid = gridplot([[p1, p2]]) show(grid)
運行結果如圖4所示。

▲圖4 代碼示例②運行結果
代碼示例②中第11行和第15行使用scatter方法進行散點圖繪製。第7行工具條中的不同工具定義,第9行數據點的不同顏色定義,第20行和第21行採用網格顯示圖形,可以提前了解這些技巧,具體使用方法在下文中會專門進行介紹。
代碼示例③
from bokeh.sampledata.iris import flowers # 配色 colormap = {'setosa': 'red', 'versicolor': 'green', 'virginica': 'blue'} colors = [colormap[x] for x in flowers['species']] # 畫布 p = figure(title = "Iris Morphology") # 繪圖 p.circle(flowers["petal_length"], flowers["petal_width"], color=colors, fill_alpha=0.2, size=10) # 其他 p.xaxis.axis_label = 'Petal Length' p.yaxis.axis_label = 'Petal Width' # 顯示 show(p)
運行結果如圖5所示。
代碼示例③再次對前面提到的鳶尾花的數據集進行分析,圖5中x軸為花瓣長度,y軸為花瓣寬度,據此可以將該散點數據聚類為3類。同時,該段代碼展示了常規圖形的繪製流程,含x、y軸的標籤。

▲圖5 代碼示例③運行結果
代碼示例④
from bokeh.layouts import column, gridplot from bokeh.models import BoxSelectTool, Div # 數據 x = np.linspace(0, 4*np.pi, 100) y = np.sin(x) # 工具條 TOOLS = "wheel_zoom,save,box_select,lasso_select,reset" # HTML代碼 div = Div(text=""" <p>Bokeh中的畫布可通過多種布局方式進行顯示;</p> <p>通過配置參數BoxSelectTool,在圖中用鼠標選擇數據,採用不同方式進行交互。</p> """) # HTML代碼直接作為一個圖層顯示,也可以作為整個HTML文檔 # 視圖屬性 opts = dict(tools=TOOLS, plot_width=350, plot_height=350) # 繪圖1 p1 = figure(title="selection on mouseup", **opts) p1.circle(x, y, color="navy", size=6, alpha=0.6) # 繪圖2 p2 = figure(title="selection on mousemove", **opts) p2.square(x, y, color="olive", size=6, alpha=0.6) p2.select_one(BoxSelectTool).select_every_mousemove = True # 繪圖3 p3 = figure(title="default highlight", **opts) p3.circle(x, y, color="firebrick", alpha=0.5, size=6) # 繪圖4 p4 = figure(title="custom highlight", **opts) p4.square(x, y, color="navy", size=6, alpha=0.6, nonselection_color="orange", nonselection_alpha=0.6) # 布局 layout = column(div, gridplot([[p1, p2], [p3, p4]], toolbar_location="right"), sizing_mode="scale_width") # sizing_mode 根據窗口寬度縮放圖像 # 繪圖 show(layout)
Bokeh中的畫布可通過多種布局方式進行顯示:通過配置視圖參數,在視圖中進行交互可視化。運行結果如圖6所示。

▲圖6 代碼示例④運行結果
代碼示例④讓讀者感受一下Bokeh的交互效果,Div方法可以直接使用HTML標籤,其作為一個獨立的圖層進行顯示(第30行)。另外需要注意,可以通過`nonselection_`,`nonselection_alpha`或`nonselection_fill_alpha`設套索置選取數據時的散點的顏色、透明度等。
代碼示例⑤
from bokeh.models import ( ColumnDataSource, Range1d, DataRange1d, LinearAxis, SingleIntervalTicker, FixedTicker, Label, Arrow, NormalHead, HoverTool, TapTool, CustomJS) from bokeh.sampledata.sprint import sprint abbrev_to_country = { "USA": "United States", "GBR": "Britain", "JAM": "Jamaica", "CAN": "Canada", "TRI": "Trinidad and Tobago", "AUS": "Australia", "GER": "Germany", "CUB": "Cuba", "NAM": "Namibia", "URS": "Soviet Union", "BAR": "Barbados", "BUL": "Bulgaria", "HUN": "Hungary", "NED": "Netherlands", "NZL": "New Zealand", "PAN": "Panama", "POR": "Portugal", "RSA": "South Africa", "EUA": "United Team of Germany", } gold_fill = "#efcf6d" gold_line = "#c8a850" silver_fill = "#cccccc" silver_line = "#b0b0b1" bronze_fill = "#c59e8a" bronze_line = "#98715d" fill_color = { "gold": gold_fill, "silver": silver_fill, "bronze": bronze_fill } line_color = { "gold": gold_line, "silver": silver_line, "bronze": bronze_line } def selected_name(name, medal, year): return name if medal == "gold" and year in [1988, 1968, 1936, 1896] else "" t0 = sprint.Time[0] sprint["Abbrev"] = sprint.Country sprint["Country"] = sprint.Abbrev.map(lambda abbr: abbrev_to_country[abbr]) sprint["Medal"] = sprint.Medal.map(lambda medal: medal.lower()) sprint["Speed"] = 100.0/sprint.Time sprint["MetersBack"] = 100.0*(1.0 - t0/sprint.Time) sprint["MedalFill"] = sprint.Medal.map(lambda medal: fill_color[medal]) sprint["MedalLine"] = sprint.Medal.map(lambda medal: line_color[medal]) sprint["SelectedName"] = sprint[["Name", "Medal", "Year"]].apply(tuple, axis=1).map(lambda args: selected_name(*args)) source = ColumnDataSource(sprint) xdr = Range1d(start=sprint.MetersBack.max()+2, end=0) # XXX: +2 is poor-man's padding (otherwise misses last tick) ydr = DataRange1d(range_padding=4, range_padding_units="absolute") plot = figure( x_range=xdr, y_range=ydr, plot_width=1000, plot_height=600, toolbar_location=None, outline_line_color=None, y_axis_type=None) plot.title.text = "Usain Bolt vs. 116 years of Olympic sprinters" plot.title.text_font_size = "14pt" plot.xaxis.ticker = SingleIntervalTicker(interval=5, num_minor_ticks=0) plot.xaxis.axis_line_color = None plot.xaxis.major_tick_line_color = None plot.xgrid.grid_line_dash = "dashed" yticker = FixedTicker(ticks=[1900, 1912, 1924, 1936, 1952, 1964, 1976, 1988, 2000, 2012]) yaxis = LinearAxis(ticker=yticker, major_tick_in=-5, major_tick_out=10) plot.add_layout(yaxis, "right") medal = plot.circle(x="MetersBack", y="Year", radius=dict(value=5, units="screen"), fill_color="MedalFill", line_color="MedalLine", fill_alpha=0.5, source=source, level="overlay") plot.text(x="MetersBack", y="Year", x_offset=10, y_offset=-5, text="SelectedName", text_align="left", text_baseline="middle", text_font_size="9pt", source=source) no_olympics_label = Label( x=7.5, y=1942, text="No Olympics in 1940 or 1944", text_align="center", text_baseline="middle", text_font_size="9pt", text_font_style="italic", text_color="silver") no_olympics = plot.add_layout(no_olympics_label) x = sprint[sprint.Year == 1900].MetersBack.min() - 0.5 arrow = Arrow(x_start=x, x_end=5, y_start=1900, y_end=1900, start=NormalHead(fill_color="black", size=6), end=None, line_width=1.5) plot.add_layout(arrow) meters_back = Label( x=5, x_offset=10, y=1900, text="Meters behind 2012 Bolt", text_align="left", text_baseline="middle", text_font_size="10pt", text_font_style="bold") plot.add_layout(meters_back) disclaimer = Label( x=0, y=0, x_units="screen", y_units="screen", text="This chart includes medals for the United States and Australia in the "Intermediary" Games of 1906, which the I.O.C. does not formally recognize.", text_font_size="8pt", text_color="silver") plot.add_layout(disclaimer, "below") tooltips = """ <div> <span style="font-size: 15px;">@Name</span> <span style="font-size: 10px; color: #666;">(@Abbrev)</span> </div> <div> <span style="font-size: 17px; font-weight: bold;">@Time{0.00}</span> <span style="font-size: 10px; color: #666;">@Year</span> </div> <div style="font-size: 11px; color: #666;">@{MetersBack}{0.00} meters behind</div> """ plot.add_tools(HoverTool(tooltips=tooltips, renderers=[medal])) open_url = CustomJS(args=dict(source=source), code=""" source.inspected._1d.indices.forEach(function(index) { var name = source.data["Name"][index]; var url = "http://en.wikipedia.org/wiki/" + encodeURIComponent(name); window.open(url); }); """) plot.add_tools(TapTool(callback=open_url, renderers=[medal], behavior="inspect")) show(plot)
運行結果如圖7所示。

▲圖7 代碼示例⑤運行結果
代碼示例⑤展示了短跑選手博爾特與116年來奧運會其他短跑選手成績的對比情況。上述代碼包含數據預處理、自定義繪圖屬性、數據標記、交互式顯示等較為複雜的操作,不作為本文重點;讀者僅需要知道通過哪些代碼可以實現哪些可視化的效果即可。
本文通過5個代碼示例展示了散點圖的繪製技巧,繪製難度也逐漸增大,與此同時,展現的效果也越來越好。讀者在學習過程中可以多思考,在這個示例中哪些數據需要交互式展示,採用哪種展示方式更好。
關於作者:屈希峰,資深Python工程師,Bokeh領域的實踐者和佈道者,對Bokeh有深入的研究。擅長Flask、MongoDB、Sklearn等技術,實踐經驗豐富。知乎多個專欄(Python中文社區、Python程序員、大數據分析挖掘)作者,專欄累計關注用戶十餘萬人。
本文摘編自《Python數據可視化:基於Bokeh的可視化繪圖》,經出版方授權發佈。