以下文章來源于大資料DT ,作者屈希峰
文章連結:
https://mp.weixin.qq.com/s/PKvsyOHhZJHAZPR-ghdNBA

01 概述
散點圖(Scatter)又稱散點分布圖,是以一個變量為橫坐标,另一個變量為縱坐标,利用散點(坐标點)的分布形态反映變量統計關系的一種圖形。
特點是能直覺表現出影響因素和預測對象之間的總體關系趨勢。優點是能通過直覺醒目的圖形方式反映變量間關系的變化形态,以便決定用何種數學表達方式來模拟變量之間的關系。散點圖不僅可傳遞變量間關系類型的資訊,還能反映變量間關系的明确程度。
通過觀察散點圖資料點的分布情況,我們可以推斷出變量間的相關性。如果變量之間不存在互相關系,那麼在散點圖上就會表現為随機分布的離散的點,如果存在某種相關性,那麼大部分的資料點就會相對密集并以某種趨勢呈現。
資料的相關關系大體上可以分為:正相關(兩個變量值同時增長)、負相關(一個變量值增加,另一個變量值下降)、不相關、線性相關、指數相關等,表現在散點圖上的大緻分布如圖1所示。那些離點叢集較遠的點我們稱之為離群點或者異常點。
▲圖1 散點資料的相關性
在Python體系中,可使用Scipy、Statsmodels或Sklearn等對離散點進行回歸分析,歸納現有資料并進行預測分析。對于那些變量之間存在密切關系,但是這些關系又不像數學公式和實體公式那樣能夠精确表達的,散點圖是一種很好的圖形工具,可以進行直覺展示,如圖2所示。
▲圖2 散點資料拟合(線性)
但是在分析過程中需要注意,變量之間的相關性并不等同于确定的因果關系,仍需要考慮其他影響因素。
02 執行個體
散點圖代碼示例如下所示。
代碼示例①
運作結果如圖3所示。
▲圖3 代碼示例①運作結果
代碼示例①中第7行使用scatter方法進行散點圖繪制;第11行采用circle方法進行散點圖繪制(推薦)。關于這兩個方法的參數說明如下。
p.circle(x, y, kwargs)參數說明。**
- x (str or seq[float]) : 離散點的x坐标,列名或清單
- y (str or seq[float]) : 離散點的y坐标
- (str or list[float]) : 離散點的大小,螢幕像素機關
- marker (str, or list[str]) : 離散點标記類型名稱或名稱清單
- color (color value, optional) : 填充及輪廓線的顔色
- source(
) : Bokeh專屬資料格式~bokeh.models.sources.ColumnDataSource
- **kwargs: 其他自定義屬性;其中标記點類型marker預設值為:“marker="circle"”,可以用“radius”定義圓的半徑大小(機關為坐标軸機關)。這在Web資料化中非常有用,不同的方式,在不同的裝置上的展示效果會有些許差異。
p.scatter(x, y, kwargs)參數說明。**
- x(:class:
) : x坐标~bokeh.core.properties.NumberSpec
- y(:class:
) : y坐标~bokeh.core.properties.NumberSpec
- angle(:class:
) : 旋轉角度~bokeh.core.properties.AngleSpec
- angle_units(:class:
) : (default: 'rad') 預設:弧度,也可以采用度('degree')~bokeh.core.enums.AngleUnits
- fill_alpha(:class:
) : (default: 1.0) 填充透明度,預設:不透明~bokeh.core.properties.NumberSpec
- fill_color(:class:
) : (default: 'gray') 填充顔色,預設:灰色~bokeh.core.properties.ColorSpec
- line_alpha(:class:
) : (default: 1.0) 輪廓線透明度,預設:不透明~bokeh.core.properties.NumberSpec
- line_cap :(:class:
) : (default: 'butt') 線端(帽)~bokeh.core.enums.LineCap
- line_color(:class:
) : (default: 'black') 輪廓線顔色,預設:黑色~bokeh.core.properties.ColorSpec
- line_dash(:class:
) : (default: []) 虛線~bokeh.core.properties.DashPattern
- line_dash_offset(:class:
) : (default: 0) 虛線偏移~bokeh.core.properties.Int
- line_join(:class:
) : (default: 'bevel')~bokeh.core.enums.LineJoin
- line_width(:class:
) : (default: 1) 線寬,預設:1~bokeh.core.properties.NumberSpec
另外,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程式員、大資料分析挖掘)作者,專欄累計關注使用者十餘萬人。