前言之前工作的时候，需要写一个日内交易信号监控软件，预计的功能是实时更新数据，出现信号时有提示功能（比如颜色变化和声音提醒）。如果可能的话最好也有可视化功能。我第一时间就想到了plotly同家出品的Dash，它是一个python Web可视化框架库，之前就有兴趣学，但是因为懒所以一直没有入手。需求来了才开始看文档，边看边翻译、实践，花了几天就写出了一个简单的网站。我个人还是挺满意的，所以也打算出一期教程介绍一下Dash。先把之前翻译的文档放上来把，之后有时间就讲一些高级用法。下文均翻译自https://dash.plotly.com/，算是一些比较基础的用法，有些地方可能是机翻的，最好还是去官网看。如果你有html和plotly基础，那入门应该会相当快，没有也没关系，我就不懂html，仿照着官网例子也能写。安装pip install dash布局基本用法首先，使用以下代码创建一个名为 app.py 的文件。import dash import dash_core_components as dcc import dash_html_components as html import plotly.express as px import pandas as pd external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css'] app = dash.Dash(__name__, external_stylesheets=external_stylesheets) # assume you have a "long-form" data frame # see https://plotly.com/python/px-arguments/ for more options df = pd.DataFrame({ "Fruit": ["Apples", "Oranges", "Bananas", "Apples", "Oranges", "Bananas"], "Amount": [4, 1, 2, 2, 4, 5], "City": ["SF", "SF", "SF", "Montreal", "Montreal", "Montreal"] }) fig = px.bar(df, x="Fruit", y="Amount", color="City", barmode="group") app.layout = html.Div(children=[ html.H1(children='Hello Dash'), html.Div(children=''' Dash: A web application framework for Python. '''), dcc.Graph( id='example-graph', figure=fig ) ]) if __name__ == '__main__': app.run_server(debug=True)然后运行程序$ python app.py访问 http://127.0.0.1:8050/效果如下： 注意：布局由一个“组件”树组成，比如 html.Div 和 dcc.Graph。对于每个 HTML 标记，dash_html_components 库都有一个组件。html.H1(children='Hello Dash')组件在应用程序中生成一个 Hello Dash HTML 元素。并非所有组件都是纯 HTML。dash_core_components 是可交互式高级组件，通过 React.js 库用 JavaScript、HTML 和 CSS 生成。每个组件都是通过关键字属性来描述的。dash 是声明式语法：主要通过这些属性描述应用程序。children 属性是特殊的。按照惯例，它总是第一个属性，这意味着您可以忽略它：html.H1(children='Hello Dash') 等价于 html.H1('Hello Dash')。它还可以包含字符串、数字、单个组件或组件列表。应用程序中的字体看起来与此处显示的字体略有不同。此应用程序使用自定义 CSS 样式表来修改元素的默认样式。您可以在 https://dash.plotly.com/external-resources 中了解更多信息，但现在您可以使用external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css'] app = dash.Dash(__name__, external_stylesheets=external_stylesheets)获得和示例相同的样式。做出第一项改变dash 包括 热加载 功能，该功能默认激活，用 app.run_server(debug=True) 启动程序后，当你修改代码时，浏览器会自动刷新你的修改。试一试：修改标题 Hello DashDon't like hot-reloading? You can turn this off with app.run_server(dev_tools_hot_reload=False)更多关于 HTML 的知识dash_html_components 库对每个 HTML 标记都有对应的组件，同样对于所有 HTML 参数都有对应的关键字参数。# -*- coding: utf-8 -*- # Run this app with `python app.py` and # visit http://127.0.0.1:8050/ in your web browser. import dash import dash_core_components as dcc import dash_html_components as html import plotly.express as px import pandas as pd external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css'] app = dash.Dash(__name__, external_stylesheets=external_stylesheets) colors = { 'background': '#111111', 'text': '#7FDBFF' } # assume you have a "long-form" data frame # see https://plotly.com/python/px-arguments/ for more options df = pd.DataFrame({ "Fruit": ["Apples", "Oranges", "Bananas", "Apples", "Oranges", "Bananas"], "Amount": [4, 1, 2, 2, 4, 5], "City": ["SF", "SF", "SF", "Montreal", "Montreal", "Montreal"] }) fig = px.bar(df, x="Fruit", y="Amount", color="City", barmode="group") fig.update_layout( plot_bgcolor=colors['background'], paper_bgcolor=colors['background'], font_color=colors['text'] ) app.layout = html.Div(style={'backgroundColor': colors['background']}, children=[ html.H1( children='Hello Dash', style={ 'textAlign': 'center', 'color': colors['text'] } ), html.Div(children='Dash: A web application framework for Python.', style={ 'textAlign': 'center', 'color': colors['text'] }), dcc.Graph( id='example-graph-2', figure=fig ) ]) if __name__ == '__main__': app.run_server(debug=True)在这个例子中，我们通过 style 属性修改了 html.Div 和 html.H1 的内联样式。html.H1('Hello Dash', style={'textAlign': 'center', 'color': '#7FDBFF'}) 转换为 HTML 代码即 Hello Dash dash_html_components 和 HTML 属性之间几乎没有重要区别：HTML 中的 style 属性是以分号分隔的字符串。在 Dash 中，你可以只提供字典。style 字典中的键为驼峰命名，所以使用 textAlign 代替 text-alignHTML class 属性在 dash 中为·className·。HTML 标记的子级是通过 children 关键字参数指定的。按照惯例，这总是第一个参数，所以它经常被省略。除此之外，所有可用的 HTML 属性和标记都可以在 Python context 中使用。可复用组件通过用 Python 编写我们的标记，我们可以创建复杂的可重用组件，如表，而无需切换上下文或语言。# Run this app with `python app.py` and # visit http://127.0.0.1:8050/ in your web browser. import dash import dash_core_components as dcc import dash_html_components as html import pandas as pd df = pd.read_csv('https://gist.githubusercontent.com/chriddyp/c78bf172206ce24f77d6363a2d754b59/raw/c353e8ef842413cae56ae3920b8fd78468aa4cb2/usa-agricultural-exports-2011.csv') def generate_table(dataframe, max_rows=10): return html.Table([ html.Thead( html.Tr([html.Th(col) for col in dataframe.columns]) ), html.Tbody([ html.Tr([ html.Td(dataframe.iloc[i][col]) for col in dataframe.columns ]) for i in range(min(len(dataframe), max_rows)) ]) ]) external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css'] app = dash.Dash(__name__, external_stylesheets=external_stylesheets) app.layout = html.Div(children=[ html.H4(children='US Agriculture Exports (2011)'), generate_table(df) ]) if __name__ == '__main__': app.run_server(debug=True)更多关于可视化的知识dash_core_components 库包括一个名为 Graph 的组件Graph 使用开源的 plotly.js JavaScript 图形库渲染可交互式数据。plotly.js 支持超过 35 种图表类型，并以矢量质量 SVG 和高性能 WebGL 呈现图表。dash_core_components.Graph 中的 figure 参数和 plotly.py 中的 figure 参数相同。# Run this app with `python app.py` and # visit http://127.0.0.1:8050/ in your web browser. import dash import dash_core_components as dcc import dash_html_components as html import plotly.express as px import pandas as pd external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css'] app = dash.Dash(__name__, external_stylesheets=external_stylesheets) df = pd.read_csv('https://gist.githubusercontent.com/chriddyp/5d1ea79569ed194d432e56108a04d188/raw/a9f9e8076b837d541398e999dcbac2b2826a81f8/gdp-life-exp-2007.csv') fig = px.scatter(df, x="gdp per capita", y="life expectancy", size="population", color="continent", hover_name="country", log_x=True, size_max=60) app.layout = html.Div([ dcc.Graph( id='life-exp-vs-gdp', figure=fig ) ]) if __name__ == '__main__': app.run_server(debug=True)效果： Markdown我们也可以直接使用 Markdown 语法来实现部分 HTML 代码# Run this app with `python app.py` and # visit http://127.0.0.1:8050/ in your web browser. import dash import dash_core_components as dcc import dash_html_components as html external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css'] app = dash.Dash(__name__, external_stylesheets=external_stylesheets) markdown_text = ''' ### Dash and Markdown Dash apps can be written in Markdown. Dash uses the [CommonMark](http://commonmark.org/) specification of Markdown. Check out their [60 Second Markdown Tutorial](http://commonmark.org/help/) if this is your first introduction to Markdown! ''' app.layout = html.Div([ dcc.Markdown(children=markdown_text) ]) if __name__ == '__main__': app.run_server(debug=True)Core Componentsdash_core_components 包括了大量的高级组件，如下拉菜单、复选框等等，详见 https://dash.plotly.com/dash-core-components# -*- coding: utf-8 -*- # Run this app with `python app.py` and # visit http://127.0.0.1:8050/ in your web browser. import dash import dash_core_components as dcc import dash_html_components as html external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css'] app = dash.Dash(__name__, external_stylesheets=external_stylesheets) app.layout = html.Div([ html.Label('Dropdown'), dcc.Dropdown( options=[ {'label': 'New York City', 'value': 'NYC'}, {'label': u'Montréal', 'value': 'MTL'}, {'label': 'San Francisco', 'value': 'SF'} ], value='MTL' ), html.Label('Multi-Select Dropdown'), dcc.Dropdown( options=[ {'label': 'New York City', 'value': 'NYC'}, {'label': u'Montréal', 'value': 'MTL'}, {'label': 'San Francisco', 'value': 'SF'} ], value=['MTL', 'SF'], multi=True ), html.Label('Radio Items'), dcc.RadioItems( options=[ {'label': 'New York City', 'value': 'NYC'}, {'label': u'Montréal', 'value': 'MTL'}, {'label': 'San Francisco', 'value': 'SF'} ], value='MTL' ), html.Label('Checkboxes'), dcc.Checklist( options=[ {'label': 'New York City', 'value': 'NYC'}, {'label': u'Montréal', 'value': 'MTL'}, {'label': 'San Francisco', 'value': 'SF'} ], value=['MTL', 'SF'] ), html.Label('Text Input'), dcc.Input(value='MTL', type='text'), html.Label('Slider'), dcc.Slider( min=0, max=9, marks={i: 'Label {}'.format(i) if i == 1 else str(i) for i in range(1, 6)}, value=5, ), ], style={'columnCount': 2}) if __name__ == '__main__': app.run_server(debug=True)value 是默认值Dash 回调本章介绍如何使用回调函数来制作 Dash 应用程序：当输入组件的属性更改时，Dash 自动调用 python 函数。简单的交互式 Dash 应用import dash import dash_core_components as dcc import dash_html_components as html from dash.dependencies import Input, Output external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css'] app = dash.Dash(__name__, external_stylesheets=external_stylesheets) app.layout = html.Div([ html.H6("Change the value in the text box to see callbacks in action!"), html.Div(["Input: ", dcc.Input(id='my-input', value='initial value', type='text')]), html.Br(), html.Div(id='my-output'), ]) @app.callback( Output(component_id='my-output', component_property='children'), [Input(component_id='my-input', component_property='value')] ) def update_output_div(input_value): return 'Output: {}'.format(input_value) if __name__ == '__main__': app.run_server(debug=True)让我们拆解这个例子：应用程序接口的“输入”和“输出”被声明为装饰器 @app.callback 的参数通过编写这个 decorator，我们告诉 Dash 只要“输入组件”（文本框）的值发生变化，就调用该函数，以更新页面上“输出”组件（HTML div）的子组件装饰器下的函数名可以任意装饰器中的 id 必须与 dash 中 layout 的 id 相同在 Dash 中，我们应用程序的输入和输出只是一个特定组件的属性。在这个例子中，我们的输入是 ID 为 "my-input "的组件的 "value "属性。我们的输出是 ID 为 "my-output "的组件的 "children "属性。每当一个输入属性发生变化时，回调装饰器封装的函数就会自动被调用。Dash 向函数提供输入属性的新值作为输入参数，Dash 则使用函数返回的任何内容更新输出组件的属性。component_id 和 component_property 关键字是可选的（每个对象只有两个参数）。为了清楚起见，在这个例子中包含了这两个关键字，但为了简洁和可读性，在文档的其余部分将省略这两个关键字。不要混淆 dash.dependencies.Input 对象和 dash_core_components.Input 对象。前者只是在这些回调中使用，后者是一个实际的组件。请注意，我们没有在布局中为 my-output 组件的 children 属性设置一个值。当 Dash 应用程序启动时，它会自动调用所有具有输入组件初始值的回调，以便填充输出组件的初始状态。在此示例中，如果您指定了类似 html.Div(id='my-output', children='Hello world') 这样的内容，当应用程序启动时，它将被覆盖。Dash App Layout With Figure and Slider让我们看看另一个例子，在这个例子中，dcc.Slider 更新了一个 dcc.Graph。import dash import dash_core_components as dcc import dash_html_components as html from dash.dependencies import Input, Output import plotly.express as px import pandas as pd df = pd.read_csv('https://cdn.jsdelivr.net/gh/plotly/datasets/gapminderDataFiveYear.csv') external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css'] app = dash.Dash(__name__, external_stylesheets=external_stylesheets) app.layout = html.Div([ dcc.Graph(id='graph-with-slider'), dcc.Slider( id='year-slider', min=df['year'].min(), max=df['year'].max(), value=df['year'].min(), marks={str(year): str(year) for year in df['year'].unique()}, step=None ) ]) @app.callback( Output('graph-with-slider', 'figure'), [Input('year-slider', 'value')]) def update_figure(selected_year): filtered_df = df[df.year == selected_year] fig = px.scatter(filtered_df, x="gdpPercap", y="lifeExp", size="pop", color="continent", hover_name="country", log_x=True, size_max=55) fig.update_layout(transition_duration=500) return fig if __name__ == '__main__': app.run_server(debug=True)效果图： 在这个例子中，Slider 的 "value "属性是应用程序的输入，应用程序的输出是 Graph 的 "figure "属性。每当 Slider 的值发生变化时，Dash 就会使用新的值调用回调函数 update_figure。该函数使用该新值过滤数据框，构造一个 figure 对象，并将其返回给 Dash 应用程序。在这个例子中，有一些不错的思路。我们使用 Pandas 库来导入和过滤内存中的数据集。我们在应用开始时加载我们的 df：df = pd.read_csv('...')。这个 df 在应用的全局状态下，可以在回调函数里面读取。将数据加载到内存中是很昂贵的。通过在应用开始时加载查询数据，而不是在回调函数里面加载查询数据，我们可以确保这个操作只在应用服务器启动时完成。当用户访问应用程序或与应用程序交互时，该数据（df）已经在内存中。如果可能的话，昂贵的初始化（比如下载或查询数据）应该在应用的全局范围内完成，而不是在回调函数内完成。回调不会修改原始数据，它只是通过 pandas 过滤器过滤来创建数据框架的副本。这一点很重要：你的回调永远不应该突变其范围外的变量。如果你的回调修改了全局状态，那么一个用户的会话可能会影响下一个用户的会话，当应用部署在多个进程或线程上时，这些修改不会在不同的会话之间共享。我们正在使用 layout.transition 开启过渡，以便让人了解数据集如何随着时间的推移而演变：过渡允许图表从一个状态平滑地更新到下一个状态，就像它是动画一样。其 transition_duration=500 参数表示过渡到下一个图表所需时间。多输入import dash import dash_core_components as dcc import dash_html_components as html from dash.dependencies import Input, Output import plotly.express as px import pandas as pd external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css'] app = dash.Dash(__name__, external_stylesheets=external_stylesheets) df = pd.read_csv('https://plotly.github.io/datasets/country_indicators.csv') available_indicators = df['Indicator Name'].unique() app.layout = html.Div([ html.Div([ html.Div([ dcc.Dropdown( id='xaxis-column', options=[{'label': i, 'value': i} for i in available_indicators], value='Fertility rate, total (births per woman)' ), dcc.RadioItems( id='xaxis-type', options=[{'label': i, 'value': i} for i in ['Linear', 'Log']], value='Linear', labelStyle={'display': 'inline-block'} ) ], style={'width': '48%', 'display': 'inline-block'}), html.Div([ dcc.Dropdown( id='yaxis-column', options=[{'label': i, 'value': i} for i in available_indicators], value='Life expectancy at birth, total (years)' ), dcc.RadioItems( id='yaxis-type', options=[{'label': i, 'value': i} for i in ['Linear', 'Log']], value='Linear', labelStyle={'display': 'inline-block'} ) ],style={'width': '48%', 'float': 'right', 'display': 'inline-block'}) ]), dcc.Graph(id='indicator-graphic'), dcc.Slider( id='year--slider', min=df['Year'].min(), max=df['Year'].max(), value=df['Year'].max(), marks={str(year): str(year) for year in df['Year'].unique()}, step=None ) ]) @app.callback( Output('indicator-graphic', 'figure'), [Input('xaxis-column', 'value'), Input('yaxis-column', 'value'), Input('xaxis-type', 'value'), Input('yaxis-type', 'value'), Input('year--slider', 'value')]) def update_graph(xaxis_column_name, yaxis_column_name, xaxis_type, yaxis_type, year_value): dff = df[df['Year'] == year_value] fig = px.scatter(x=dff[dff['Indicator Name'] == xaxis_column_name]['Value'], y=dff[dff['Indicator Name'] == yaxis_column_name]['Value'], hover_name=dff[dff['Indicator Name'] == yaxis_column_name]['Country Name']) fig.update_layout(margin={'l': 40, 'b': 40, 't': 10, 'r': 0}, hovermode='closest') fig.update_xaxes(title=xaxis_column_name, type='linear' if xaxis_type == 'Linear' else 'log') fig.update_yaxes(title=yaxis_column_name, type='linear' if yaxis_type == 'Linear' else 'log') return fig if __name__ == '__main__': app.run_server(debug=True)在这个例子中，每当 Dropdown、Slider 或 RadioItems 组件的值属性改变时， update_graph 函数就会被调用。update_graph 函数的输入参数是每个 Input 属性的新值或当前值，按照它们被指定的顺序。即使一次只改变一个单一的输入（用户在给定的时刻只能改变一个 Dropdown 的值），Dash 也会收集所有指定的输入属性的当前状态，并为您将它们传递到您的函数中。你的回调函数总是被保证传递给应用程序的代表状态。让我们扩展我们的例子以包括多个输出。多输出到目前为止，我们写的所有回调都只更新一个输出属性。我们也可以同时更新几个属性：把所有要更新的属性作为一个列表放在装饰器中，然后从回调中返回这么多项目。如果两个输出依赖于同一个计算密集的中间结果，比如一个缓慢的数据库查询，就会表现得特别好。import dash import dash_core_components as dcc import dash_html_components as html from dash.dependencies import Input, Output external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css'] app = dash.Dash(__name__, external_stylesheets=external_stylesheets) app.layout = html.Div([ dcc.Input( id='num-multi', type='number', value=5 ), html.Table([ html.Tr([html.Td(['x', html.Sup(2)]), html.Td(id='square')]), html.Tr([html.Td(['x', html.Sup(3)]), html.Td(id='cube')]), html.Tr([html.Td([2, html.Sup('x')]), html.Td(id='twos')]), html.Tr([html.Td([3, html.Sup('x')]), html.Td(id='threes')]), html.Tr([html.Td(['x', html.Sup('x')]), html.Td(id='x^x')]), ]), ]) @app.callback( [Output('square', 'children'), Output('cube', 'children'), Output('twos', 'children'), Output('threes', 'children'), Output('x^x', 'children')], [Input('num-multi', 'value')]) def callback_a(x): return x**2, x**3, 2**x, 3**x, x**x if __name__ == '__main__': app.run_server(debug=True)需要提醒的是：即使可以合并输出，也不一定是个好主意。如果输出依赖于一些而不是所有相同的 Inputs，将它们分开可以避免不必要的更新。如果它们有相同的 Inputs，但用这些 Inputs 做独立的计算，保持回调分离可以让它们并行运行。链式回调你也可以将输出和输入链在一起：一个回调函数的输出可以是另一个回调函数的输入。这种模式可以用来创建动态用户界面，其中一个输入组件更新下一个输入组件的可用选项。下面是一个简单的例子。# -*- coding: utf-8 -*- import dash import dash_core_components as dcc import dash_html_components as html from dash.dependencies import Input, Output external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css'] app = dash.Dash(__name__, external_stylesheets=external_stylesheets) all_options = { 'America': ['New York City', 'San Francisco', 'Cincinnati'], 'Canada': [u'Montréal', 'Toronto', 'Ottawa'] } app.layout = html.Div([ dcc.RadioItems( id='countries-radio', options=[{'label': k, 'value': k} for k in all_options.keys()], value='America' ), html.Hr(), dcc.RadioItems(id='cities-radio'), html.Hr(), # 横线 html.Div(id='display-selected-values') ]) @app.callback( Output('cities-radio', 'options'), [Input('countries-radio', 'value')]) def set_cities_options(selected_country): return [{'label': i, 'value': i} for i in all_options[selected_country]] @app.callback( Output('cities-radio', 'value'), [Input('cities-radio', 'options')]) def set_cities_value(available_options): return available_options[0]['value'] @app.callback( Output('display-selected-values', 'children'), [Input('countries-radio', 'value'), Input('cities-radio', 'value')]) def set_display_children(selected_country, selected_city): return u'{} is a city in {}'.format( selected_city, selected_country, ) if __name__ == '__main__': app.run_server(debug=True)第一个回调根据第一个 RadioItems 组件中的选择值更新第二个 RadioItems 组件中的可用选项。第二个回调在选项属性改变时设置一个初始值：它将其设置为该选项数组中的第一个值。最后一个回调显示每个组件的选定值。如果您更改了 countries RadioItems 组件的值，Dash 将等待直到 cities 组件的值更新后再调用最终回调。这可以防止你的回调被调用时状态不一致，比如 "America "和 "Montréal"。状态在某些情况下，你可能会在你的应用程序中使用一个 "表单 "类型的模式。在这种情况下，你可能想读取输入组件的值，但只有当用户完成了他或她在表单中的所有信息输入时才会读取。直接给输入值附加一个回调可以实现这一点。# -*- coding: utf-8 -*- import dash import dash_core_components as dcc import dash_html_components as html from dash.dependencies import Input, Output external_stylesheets = ["https://codepen.io/chriddyp/pen/bWLwgP.css"] app = dash.Dash(__name__, external_stylesheets=external_stylesheets) app.layout = html.Div( [ dcc.Input(id="input-1", type="text", value="Montréal"), dcc.Input(id="input-2", type="text", value="Canada"), html.Div(id="number-output"), ] ) @app.callback( Output("number-output", "children"), [Input("input-1", "value"), Input("input-2", "value")], ) def update_output(input1, input2): return u'Input 1 is "{}" and Input 2 is "{}"'.format(input1, input2) if __name__ == "__main__": app.run_server(debug=True) 在这个例子中，每当 dash.dependencies.Input 描述的任何属性发生变化时，回调函数就会被触发。你可以在上面的输入中输入数据来试试。dash.dependencies.State 允许你在不触发回调的情况下传递额外的值。这里的例子和上面的一样，但是把 dcc.Input 作为 dash.dependencies.State，把按钮作为 dash.dependencies.Input。# -*- coding: utf-8 -*- import dash import dash_core_components as dcc import dash_html_components as html from dash.dependencies import Input, Output, State external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css'] app = dash.Dash(__name__, external_stylesheets=external_stylesheets) app.layout = html.Div([ dcc.Input(id='input-1-state', type='text', value='Montréal'), dcc.Input(id='input-2-state', type='text', value='Canada'), html.Button(id='submit-button-state', n_clicks=0, children='Submit'), html.Div(id='output-state') ]) @app.callback(Output('output-state', 'children'), [Input('submit-button-state', 'n_clicks')], [State('input-1-state', 'value'), State('input-2-state', 'value')]) def update_output(n_clicks, input1, input2): return u''' The Button has been pressed {} times, Input 1 is "{}", and Input 2 is "{}" '''.format(n_clicks, input1, input2) if __name__ == '__main__': app.run_server(debug=True) 在这个例子中，改变 dcc.Input 框中的文本不会触发回调，但点击按钮会。尽管 dcc.Input 值的当前值没有触发回调函数本身，但它们仍然会被传递到回调中。请注意，我们是通过监听 html.Button 组件的 n_clicks 属性来触发回调的。n_clicks 是一个每次组件被单击时都会递增的属性。dash_html_components 库中的每个组件都有这个属性。可交互式可视化可交互式作图dash_core_components 库中包含一个名为 Graph 的组件。Graph 使用开源的 plotly.js JavaScript 图形库来渲染交互式数据可视化。Plotly.js 支持超过 35 种图表类型，并以矢量质量的 SVG 和高性能的 WebGL 渲染图表。dash_core_components.Graph 组件中的 figure 参数与 Plotly 的开源 Python 图表库 plotly.py 使用的 figure 参数相同。查看 plotly.py 文档和图库以了解更多。Dash 组件由一组属性声明性地描述。所有这些属性都可以通过回调函数更新，但只有一个子集的属性是通过用户交互更新的，比如当你点击 dcc.Dropdown 组件中的一个选项时，该组件的值属性就会改变。dcc.Graph 组件有四个属性可以通过用户交互来改变：hoverData、clickData、selectedData、 relayoutData。当你将鼠标悬停在点上，点击点，或者选择图形中的点的区域时，这些属性就会更新。import json import dash import dash_core_components as dcc import dash_html_components as html from dash.dependencies import Input, Output import plotly.express as px import pandas as pd external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css'] app = dash.Dash(__name__, external_stylesheets=external_stylesheets) styles = { 'pre': { 'border': 'thin lightgrey solid', 'overflowX': 'scroll' } } df = pd.DataFrame({ "x": [1,2,1,2], "y": [1,2,3,4], "customdata": [1,2,3,4], "fruit": ["apple", "apple", "orange", "orange"] }) fig = px.scatter(df, x="x", y="y", color="fruit", custom_data=["customdata"]) fig.update_layout(clickmode='event+select') fig.update_traces(marker_size=20) app.layout = html.Div([ dcc.Graph( id='basic-interactions', figure=fig ), html.Div(className='row', children=[ html.Div([ dcc.Markdown(""" **Hover Data** Mouse over values in the graph. """), html.Pre(id='hover-data', style=styles['pre']) ], className='three columns'), html.Div([ dcc.Markdown(""" **Click Data** Click on points in the graph. """), html.Pre(id='click-data', style=styles['pre']), ], className='three columns'), html.Div([ dcc.Markdown(""" **Selection Data** Choose the lasso or rectangle tool in the graph's menu bar and then select points in the graph. Note that if `layout.clickmode = 'event+select'`, selection data also accumulates (or un-accumulates) selected data if you hold down the shift button while clicking. """), html.Pre(id='selected-data', style=styles['pre']), ], className='three columns'), html.Div([ dcc.Markdown(""" **Zoom and Relayout Data** Click and drag on the graph to zoom or click on the zoom buttons in the graph's menu bar. Clicking on legend items will also fire this event. """), html.Pre(id='relayout-data', style=styles['pre']), ], className='three columns') ]) ]) @app.callback( Output('hover-data', 'children'), [Input('basic-interactions', 'hoverData')]) def display_hover_data(hoverData): return json.dumps(hoverData, indent=2) @app.callback( Output('click-data', 'children'), [Input('basic-interactions', 'clickData')]) def display_click_data(clickData): return json.dumps(clickData, indent=2) @app.callback( Output('selected-data', 'children'), [Input('basic-interactions', 'selectedData')]) def display_selected_data(selectedData): return json.dumps(selectedData, indent=2) @app.callback( Output('relayout-data', 'children'), [Input('basic-interactions', 'relayoutData')]) def display_relayout_data(relayoutData): return json.dumps(relayoutData, indent=2) if __name__ == '__main__': app.run_server(debug=True)Update Graphs on Hoverimport dash import dash_core_components as dcc import dash_html_components as html import pandas as pd import plotly.express as px external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css'] app = dash.Dash(__name__, external_stylesheets=external_stylesheets) df = pd.read_csv('https://cdn.jsdelivr.net/gh/plotly/datasets/country_indicators.csv') available_indicators = df['Indicator Name'].unique() app.layout = html.Div([ html.Div([ html.Div([ dcc.Dropdown( id='crossfilter-xaxis-column', options=[{'label': i, 'value': i} for i in available_indicators], value='Fertility rate, total (births per woman)' ), dcc.RadioItems( id='crossfilter-xaxis-type', options=[{'label': i, 'value': i} for i in ['Linear', 'Log']], value='Linear', labelStyle={'display': 'inline-block'} ) ], style={'width': '49%', 'display': 'inline-block'}), html.Div([ dcc.Dropdown( id='crossfilter-yaxis-column', options=[{'label': i, 'value': i} for i in available_indicators], value='Life expectancy at birth, total (years)' ), dcc.RadioItems( id='crossfilter-yaxis-type', options=[{'label': i, 'value': i} for i in ['Linear', 'Log']], value='Linear', labelStyle={'display': 'inline-block'} ) ], style={'width': '49%', 'float': 'right', 'display': 'inline-block'}) ], style={ 'borderBottom': 'thin lightgrey solid', 'backgroundColor': 'rgb(250, 250, 250)', 'padding': '10px 5px' }), html.Div([ dcc.Graph( id='crossfilter-indicator-scatter', hoverData={'points': [{'customdata': 'Japan'}]} ) ], style={'width': '49%', 'display': 'inline-block', 'padding': '0 20'}), html.Div([ dcc.Graph(id='x-time-series'), dcc.Graph(id='y-time-series'), ], style={'display': 'inline-block', 'width': '49%'}), html.Div(dcc.Slider( id='crossfilter-year--slider', min=df['Year'].min(), max=df['Year'].max(), value=df['Year'].max(), marks={str(year): str(year) for year in df['Year'].unique()}, step=None ), style={'width': '49%', 'padding': '0px 20px 20px 20px'}) ]) @app.callback( dash.dependencies.Output('crossfilter-indicator-scatter', 'figure'), [dash.dependencies.Input('crossfilter-xaxis-column', 'value'), dash.dependencies.Input('crossfilter-yaxis-column', 'value'), dash.dependencies.Input('crossfilter-xaxis-type', 'value'), dash.dependencies.Input('crossfilter-yaxis-type', 'value'), dash.dependencies.Input('crossfilter-year--slider', 'value')]) def update_graph(xaxis_column_name, yaxis_column_name, xaxis_type, yaxis_type, year_value): dff = df[df['Year'] == year_value] fig = px.scatter(x=dff[dff['Indicator Name'] == xaxis_column_name]['Value'], y=dff[dff['Indicator Name'] == yaxis_column_name]['Value'], hover_name=dff[dff['Indicator Name'] == yaxis_column_name]['Country Name'] ) fig.update_traces(customdata=dff[dff['Indicator Name'] == yaxis_column_name]['Country Name']) fig.update_xaxes(title=xaxis_column_name, type='linear' if xaxis_type == 'Linear' else 'log') fig.update_yaxes(title=yaxis_column_name, type='linear' if yaxis_type == 'Linear' else 'log') fig.update_layout(margin={'l': 40, 'b': 40, 't': 10, 'r': 0}, hovermode='closest') return fig def create_time_series(dff, axis_type, title): fig = px.scatter(dff, x='Year', y='Value') fig.update_traces(mode='lines+markers') fig.update_xaxes(showgrid=False) fig.update_yaxes(type='linear' if axis_type == 'Linear' else 'log') fig.add_annotation(x=0, y=0.85, xanchor='left', yanchor='bottom', xref='paper', yref='paper', showarrow=False, align='left', bgcolor='rgba(255, 255, 255, 0.5)', text=title) fig.update_layout(height=225, margin={'l': 20, 'b': 30, 'r': 10, 't': 10}) return fig @app.callback( dash.dependencies.Output('x-time-series', 'figure'), [dash.dependencies.Input('crossfilter-indicator-scatter', 'hoverData'), dash.dependencies.Input('crossfilter-xaxis-column', 'value'), dash.dependencies.Input('crossfilter-xaxis-type', 'value')]) def update_y_timeseries(hoverData, xaxis_column_name, axis_type): country_name = hoverData['points'][0]['customdata'] dff = df[df['Country Name'] == country_name] dff = dff[dff['Indicator Name'] == xaxis_column_name] title = '{} {}'.format(country_name, xaxis_column_name) return create_time_series(dff, axis_type, title) @app.callback( dash.dependencies.Output('y-time-series', 'figure'), [dash.dependencies.Input('crossfilter-indicator-scatter', 'hoverData'), dash.dependencies.Input('crossfilter-yaxis-column', 'value'), dash.dependencies.Input('crossfilter-yaxis-type', 'value')]) def update_x_timeseries(hoverData, yaxis_column_name, axis_type): dff = df[df['Country Name'] == hoverData['points'][0]['customdata']] dff = dff[dff['Indicator Name'] == yaxis_column_name] return create_time_series(dff, axis_type, yaxis_column_name) if __name__ == '__main__': app.run_server(debug=True) 试着将鼠标放在左边散点图中的点上。注意右边的线图是如何根据你悬停的点进行更新的。交叉过滤import dash import dash_core_components as dcc import dash_html_components as html import numpy as np import pandas as pd from dash.dependencies import Input, Output import plotly.express as px external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css'] app = dash.Dash(__name__, external_stylesheets=external_stylesheets) # make a sample data frame with 6 columns np.random.seed(0) df = pd.DataFrame({"Col " + str(i+1): np.random.rand(30) for i in range(6)}) app.layout = html.Div([ html.Div( dcc.Graph(id='g1', config={'displayModeBar': False}), className='four columns' ), html.Div( dcc.Graph(id='g2', config={'displayModeBar': False}), className='four columns' ), html.Div( dcc.Graph(id='g3', config={'displayModeBar': False}), className='four columns' ) ], className='row') def get_figure(df, x_col, y_col, selectedpoints, selectedpoints_local): if selectedpoints_local and selectedpoints_local['range']: ranges = selectedpoints_local['range'] selection_bounds = {'x0': ranges['x'][0], 'x1': ranges['x'][1], 'y0': ranges['y'][0], 'y1': ranges['y'][1]} else: selection_bounds = {'x0': np.min(df[x_col]), 'x1': np.max(df[x_col]), 'y0': np.min(df[y_col]), 'y1': np.max(df[y_col])} # set which points are selected with the `selectedpoints` property # and style those points with the `selected` and `unselected` # attribute. see # https://medium.com/@plotlygraphs/notes-from-the-latest-plotly-js-release-b035a5b43e21 # for an explanation fig = px.scatter(df, x=df[x_col], y=df[y_col], text=df.index) fig.update_traces(selectedpoints=selectedpoints, customdata=df.index, mode='markers+text', marker={ 'color': 'rgba(0, 116, 217, 0.7)', 'size': 20 }, unselected={'marker': { 'opacity': 0.3 }, 'textfont': { 'color': 'rgba(0, 0, 0, 0)' }}) fig.update_layout(margin={'l': 20, 'r': 0, 'b': 15, 't': 5}, dragmode='select', hovermode=False) fig.add_shape(dict({'type': 'rect', 'line': { 'width': 1, 'dash': 'dot', 'color': 'darkgrey' }}, **selection_bounds)) return fig # this callback defines 3 figures # as a function of the intersection of their 3 selections @app.callback( [Output('g1', 'figure'), Output('g2', 'figure'), Output('g3', 'figure')], [Input('g1', 'selectedData'), Input('g2', 'selectedData'), Input('g3', 'selectedData')] ) def callback(selection1, selection2, selection3): selectedpoints = df.index for selected_data in [selection1, selection2, selection3]: if selected_data and selected_data['points']: selectedpoints = np.intersect1d(selectedpoints, [p['customdata'] for p in selected_data['points']]) return [get_figure(df, "Col 1", "Col 2", selectedpoints, selection1), get_figure(df, "Col 3", "Col 4", selectedpoints, selection2), get_figure(df, "Col 5", "Col 6", selectedpoints, selection3)] if __name__ == '__main__': app.run_server(debug=True) 试着在任何一个图中点击并拖动以过滤不同的区域。在每次选择时，三个图形回调都会与每个图的最新选定区域一起启动。pandas 数据框会根据所选点进行过滤，并重新绘制图形，高亮显示所选点，并将所选区域画成虚线矩形。回调间共享数据在回调入门指南中提到，Dash 核心原则之一是 Dash 回调决不能修改其作用域之外的变量。修改任何全局变量都是不安全的。本章解释了为什么，并提供了一些在回调之间共享状态的替代模式。为什么分享状态？在某些应用程序中，您可能有多个回调，这些回调依赖于昂贵的数据处理任务，如进行 SQL 查询、运行模拟或下载数据。与其让每个回调运行同样昂贵的任务，不如让一个回调运行任务，然后将结果分享给其他回调。现在这种需求已经得到了一定的改善，你可以为一个回调设置多个输出。这样一来，那个昂贵的任务就可以做一次，并立即用于所有的输出。但是在某些情况下，这仍然不是很理想，比如说如果有简单的后续任务修改结果，比如单位转换。我们不应该为了把结果从华氏度改成摄氏度而重复一个大型数据库查询！为什么全局变量会打断你的 app？Dash 被设计为在多用户环境中工作，在这种环境中，多人可能会同时查看应用程序，并且会有独立的会话。如果您的应用程序使用修改过的全局变量，那么一个用户的会话可以将变量设置为一个值，这将影响下一个用户的会话。Dash 还被设计成能够与多个 python Worker 一起运行，这样回调就可以并行执行。这通常是通过使用 gunicorn 来实现的，使用的语法包括$ gunicorn --workers 4 app:server(app refers to a file named app.py and server refers to a variable in that file named server: server = app.server).当 Dash 应用程序在多个 Worker 之间运行时，它们的内存是不共享的。这意味着，如果您在一个回调中修改了全局变量，则该修改将不会应用于其他 Worker。这里是一个应用程序的草图，它有一个回调，可以修改其范围外的数据。由于上述原因，这种类型的模式*不会可靠地工作df = pd.DataFrame({ 'a': [1, 2, 3], 'b': [4, 1, 4], 'c': ['x', 'y', 'z'], }) app.layout = html.Div([ dcc.Dropdown( id='dropdown', options=[{'label': i, 'value': i} for i in df['c'].unique()], value='a' ), html.Div(id='output'), ]) @app.callback(Output('output', 'children'), [Input('dropdown', 'value')]) def update_output_1(value): # Here, `df` is an example of a variable that is # "outside the scope of this function". # *It is not safe to modify or reassign this variable # inside this callback.* global df = df[df['c'] == value] # do not do this, this is not safe! return len(df)要修复这个例子，只需将过滤器重新分配到回调内的一个新变量，或者遵循本指南下一部分中概述的策略之一。df = pd.DataFrame({ 'a': [1, 2, 3], 'b': [4, 1, 4], 'c': ['x', 'y', 'z'], }) app.layout = html.Div([ dcc.Dropdown( id='dropdown', options=[{'label': i, 'value': i} for i in df['c'].unique()], value='a' ), html.Div(id='output'), ]) @app.callback(Output('output', 'children'), [Input('dropdown', 'value')]) def update_output_1(value): # Safely reassign the filter to a new variable filtered_df = df[df['c'] == value] return len(filtered_df)回调间共享数据为了在多个 python 进程之间安全地共享数据，我们需要将数据存储在每个进程都能访问的地方。有三个主要的地方可以存储这些数据。1 - 在用户的浏览器会话中2 - 在磁盘上（例如，在一个文件或新的数据库上）。3 - 在共享内存空间中，比如 Redis。以下三个例子说明了这些方法。案例 1：在浏览器中使用隐藏的 Div 存储数据。通过在 https://community.plotly.com/t/sharing-a-dataframe-between-plots/6173 中解释的方法将数据保存为 Dash 前端存储的一部分来实现。数据必须转换为 JSON 这样的字符串进行存储和传输。以这种方式缓存的数据将只在用户当前会话中可用。如果你打开一个新的浏览器，应用程序的回调将始终计算数据。数据只在会话内的回调之间进行缓存和传输。因此，与缓存不同，这种方法不会增加应用程序的内存占用。在网络传输方面可能会有成本。如果你在回调之间共享 10MB 的数据，那么这些数据将在每个回调之间通过网络传输。如果网络成本太高，那就在前期计算聚合并传输这些。你的应用很可能不会显示 10MB 的数据，它只是显示其中的一个子集或聚合。这个例子概述了如何在一个回调中执行一个昂贵的数据处理步骤，将输出序列化为 JSON，并将其作为输入提供给其他回调。这个例子使用标准的 Dash 回调，并将 JSON 化的数据存储在应用程序中的一个隐藏的 div 中。global_df = pd.read_csv('...') app.layout = html.Div([ dcc.Graph(id='graph'), html.Table(id='table'), dcc.Dropdown(id='dropdown'), # Hidden div inside the app that stores the intermediate value html.Div(id='intermediate-value', style={'display': 'none'}) ]) @app.callback(Output('intermediate-value', 'children'), [Input('dropdown', 'value')]) def clean_data(value): # some expensive clean data step cleaned_df = your_expensive_clean_or_compute_step(value) # more generally, this line would be # json.dumps(cleaned_df) return cleaned_df.to_json(date_format='iso', orient='split') @app.callback(Output('graph', 'figure'), [Input('intermediate-value', 'children')]) def update_graph(jsonified_cleaned_data): # more generally, this line would be # json.loads(jsonified_cleaned_data) dff = pd.read_json(jsonified_cleaned_data, orient='split') figure = create_figure(dff) return figure @app.callback(Output('table', 'children'), [Input('intermediate-value', 'children')]) def update_table(jsonified_cleaned_data): dff = pd.read_json(jsonified_cleaned_data, orient='split') table = create_table(dff) return table 案例 2：预先计算如果数据很大，通过网络发送计算数据可能会很昂贵。在某些情况下，将这些数据和 JSON 序列化也会很昂贵。在许多情况下，您的应用程序将只显示计算或过滤数据的一个子集或聚合。在这些情况下，你可以在你的数据处理回调中预先计算你的聚合，并将这些聚合传输到其余回调中。下面是一个简单的例子，说明如何将过滤的或聚合的数据传输到多个回调。@app.callback( Output('intermediate-value', 'children'), [Input('dropdown', 'value')]) def clean_data(value): # an expensive query step cleaned_df = your_expensive_clean_or_compute_step(value) # a few filter steps that compute the data # as it's needed in the future callbacks df_1 = cleaned_df[cleaned_df['fruit'] == 'apples'] df_2 = cleaned_df[cleaned_df['fruit'] == 'oranges'] df_3 = cleaned_df[cleaned_df['fruit'] == 'figs'] datasets = { 'df_1': df_1.to_json(orient='split', date_format='iso'), 'df_2': df_2.to_json(orient='split', date_format='iso'), 'df_3': df_3.to_json(orient='split', date_format='iso'), } return json.dumps(datasets) @app.callback( Output('graph', 'figure'), [Input('intermediate-value', 'children')]) def update_graph_1(jsonified_cleaned_data): datasets = json.loads(jsonified_cleaned_data) dff = pd.read_json(datasets['df_1'], orient='split') figure = create_figure_1(dff) return figure @app.callback( Output('graph', 'figure'), [Input('intermediate-value', 'children')]) def update_graph_2(jsonified_cleaned_data): datasets = json.loads(jsonified_cleaned_data) dff = pd.read_json(datasets['df_2'], orient='split') figure = create_figure_2(dff) return figure @app.callback( Output('graph', 'figure'), [Input('intermediate-value', 'children')]) def update_graph_3(jsonified_cleaned_data): datasets = json.loads(jsonified_cleaned_data) dff = pd.read_json(datasets['df_3'], orient='split') figure = create_figure_3(dff) return figure案例 3：缓存和信号传递这个例子：通过 Flask -Cache 使用 Redis 来存储 "全局变量"。这些数据通过一个函数进行访问，函数的输出被缓存，并通过其输入参数进行键控。当昂贵的计算完成后，使用隐藏的 div 方案向其他回调发送信号。注意，你也可以不使用 Redis，而是将其保存到文件系统中。更多细节请参见 https://flask-caching.readthedocs.io/en/latest/。这种 "信号传递 "很酷，因为它允许昂贵的计算只占用一个进程。如果没有这种类型的信令，每个回调最终可能会并行计算昂贵的计算，锁定四个进程而不是一个进程。这种方法的优势还在于，未来的会话可以使用预先计算的值。这对于那些输入数量较少的应用来说，会有很好的效果。下面是一个示例。一些需要注意的事情。我通过使用 time.sleep(5)模拟了一个昂贵的过程。当应用程序加载时，渲染所有四张图需要 5 秒。初始计算只阻止了一个进程。一旦计算完成，就会发送信号，并并行执行四个回调来渲染图形。每一个回调都会从 "全局存储"：Redis 或文件系统缓存中检索数据。我在 app.run_server 中设置了 processes=6，这样就可以并行执行多个回调。在生产中，可以使用类似 $ gunicorn --workers 6 --threads 2 app:server 这样的方法来完成。如果过去已经选择了一个值，那么在下拉菜单中选择该值的时间将少于 5 秒。这是因为该值是从缓存中提取的。同样，重新加载页面或在新窗口中打开应用程序也很快，因为初始状态和初始昂贵的计算已经被计算过了。import os import copy import time import datetime import dash import dash_core_components as dcc import dash_html_components as html import numpy as np import pandas as pd from dash.dependencies import Input, Output from flask_caching import Cache external_stylesheets = [ # Dash CSS 'https://codepen.io/chriddyp/pen/bWLwgP.css', # Loading screen CSS 'https://codepen.io/chriddyp/pen/brPBPO.css'] app = dash.Dash(__name__, external_stylesheets=external_stylesheets) CACHE_CONFIG = { # try 'filesystem' if you don't want to setup redis 'CACHE_TYPE': 'redis', 'CACHE_REDIS_URL': os.environ.get('REDIS_URL', 'redis://localhost:6379') } cache = Cache() cache.init_app(app.server, config=CACHE_CONFIG) N = 100 df = pd.DataFrame({ 'category': ( (['apples'] * 5 * N) + (['oranges'] * 10 * N) + (['figs'] * 20 * N) + (['pineapples'] * 15 * N) ) }) df['x'] = np.random.randn(len(df['category'])) df['y'] = np.random.randn(len(df['category'])) app.layout = html.Div([ dcc.Dropdown( id='dropdown', options=[{'label': i, 'value': i} for i in df['category'].unique()], value='apples' ), html.Div([ html.Div(dcc.Graph(id='graph-1'), className="six columns"), html.Div(dcc.Graph(id='graph-2'), className="six columns"), ], className="row"), html.Div([ html.Div(dcc.Graph(id='graph-3'), className="six columns"), html.Div(dcc.Graph(id='graph-4'), className="six columns"), ], className="row"), # hidden signal value html.Div(id='signal', style={'display': 'none'}) ]) # perform expensive computations in this "global store" # these computations are cached in a globally available # redis memory store which is available across processes # and for all time. @cache.memoize() def global_store(value): # simulate expensive query print('Computing value with {}'.format(value)) time.sleep(5) return df[df['category'] == value] def generate_figure(value, figure): fig = copy.deepcopy(figure) filtered_dataframe = global_store(value) fig['data'][0]['x'] = filtered_dataframe['x'] fig['data'][0]['y'] = filtered_dataframe['y'] fig['layout'] = {'margin': {'l': 20, 'r': 10, 'b': 20, 't': 10}} return fig @app.callback(Output('signal', 'children'), [Input('dropdown', 'value')]) def compute_value(value): # compute value and send a signal when done global_store(value) return value @app.callback(Output('graph-1', 'figure'), [Input('signal', 'children')]) def update_graph_1(value): # generate_figure gets data from `global_store`. # the data in `global_store` has already been computed # by the `compute_value` callback and the result is stored # in the global redis cached return generate_figure(value, { 'data': [{ 'type': 'scatter', 'mode': 'markers', 'marker': { 'opacity': 0.5, 'size': 14, 'line': {'border': 'thin darkgrey solid'} } }] }) @app.callback(Output('graph-2', 'figure'), [Input('signal', 'children')]) def update_graph_2(value): return generate_figure(value, { 'data': [{ 'type': 'scatter', 'mode': 'lines', 'line': {'shape': 'spline', 'width': 0.5}, }] }) @app.callback(Output('graph-3', 'figure'), [Input('signal', 'children')]) def update_graph_3(value): return generate_figure(value, { 'data': [{ 'type': 'histogram2d', }] }) @app.callback(Output('graph-4', 'figure'), [Input('signal', 'children')]) def update_graph_4(value): return generate_figure(value, { 'data': [{ 'type': 'histogram2dcontour', }] }) if __name__ == '__main__': app.run_server(debug=True, processes=6) 案例 4：服务器上基于用户的会话数据实时更新数据dash_core_components.Interval component 组件Dash 中的组件通常通过用户交互来更新：选择下拉、拖动滑块、悬停在点上。如果您正在构建一个用于监控的应用程序，您可能希望每隔几秒或几分钟更新一次应用程序中的组件。dash_core_components.Interval 元素允许你在预定义的时间间隔上更新组件。n_intervals 属性是一个整数，每次间隔的毫秒数过后都会自动递增。您可以在应用程序的回调中监听这个变量，以便在预定义的时间间隔上启动回调。这个例子从实时卫星馈送中提取数据，并每秒钟更新一次图表和文本。import datetime import dash import dash_core_components as dcc import dash_html_components as html import plotly from dash.dependencies import Input, Output # pip install pyorbital from pyorbital.orbital import Orbital satellite = Orbital('TERRA') external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css'] app = dash.Dash(__name__, external_stylesheets=external_stylesheets) app.layout = html.Div( html.Div([ html.H4('TERRA Satellite Live Feed'), html.Div(id='live-update-text'), dcc.Graph(id='live-update-graph'), dcc.Interval( id='interval-component', interval=5*1000, # 5秒钟间隔 n_intervals=0 ) ]) ) @app.callback(Output('live-update-text', 'children'), [Input('interval-component', 'n_intervals')]) def update_metrics(n): lon, lat, alt = satellite.get_lonlatalt(datetime.datetime.now()) style = {'padding': '5px', 'fontSize': '16px'} return [ html.Span('Longitude: {0:.2f}'.format(lon), style=style), html.Span('Latitude: {0:.2f}'.format(lat), style=style), html.Span('Altitude: {0:0.2f}'.format(alt), style=style) ] # Multiple components can update everytime interval gets fired. @app.callback(Output('live-update-graph', 'figure'), [Input('interval-component', 'n_intervals')]) def update_graph_live(n): satellite = Orbital('TERRA') data = { 'time': [], 'Latitude': [], 'Longitude': [], 'Altitude': [] } # Collect some data for i in range(180): time = datetime.datetime.now() - datetime.timedelta(seconds=i*20) lon, lat, alt = satellite.get_lonlatalt( time ) data['Longitude'].append(lon) data['Latitude'].append(lat) data['Altitude'].append(alt) data['time'].append(time) # Create the graph with subplots fig = plotly.tools.make_subplots(rows=2, cols=1, vertical_spacing=0.2) fig['layout']['margin'] = { 'l': 30, 'r': 10, 'b': 30, 't': 10 } fig['layout']['legend'] = {'x': 0, 'y': 1, 'xanchor': 'left'} fig.append_trace({ 'x': data['time'], 'y': data['Altitude'], 'name': 'Altitude', 'mode': 'lines+markers', 'type': 'scatter' }, 1, 1) fig.append_trace({ 'x': data['Longitude'], 'y': data['Latitude'], 'text': data['time'], 'name': 'Longitude vs Latitude', 'mode': 'lines+markers', 'type': 'scatter' }, 2, 1) return fig if __name__ == '__main__': app.run_server(debug=True)上面说过，我们不能修改全局变量，但当全局变量是个字典时，你可以用它来存储新增的数据，以此实现依赖上一个数据计算的情况。比如：data = { 'time': [], 'Latitude': [], 'Longitude': [], 'Altitude': [] } # Multiple components can update everytime interval gets fired. @app.callback(Output('live-update-graph', 'figure'), [Input('interval-component', 'n_intervals')]) def update_graph_live(n): satellite = Orbital('TERRA') # Collect some data time = datetime.datetime.now() - datetime.timedelta(seconds=i*20) lon, lat, alt = satellite.get_lonlatalt( time ) data['Longitude'].append(lon) data['Latitude'].append(lat) data['Altitude'].append(alt) data['time'].append(time) # Create the graph with subplots fig = plotly.tools.make_subplots(rows=2, cols=1, vertical_spacing=0.2) fig['layout']['margin'] = { 'l': 30, 'r': 10, 'b': 30, 't': 10 } fig['layout']['legend'] = {'x': 0, 'y': 1, 'xanchor': 'left'} fig.append_trace({ 'x': data['time'], 'y': data['Altitude'], 'name': 'Altitude', 'mode': 'lines+markers', 'type': 'scatter' }, 1, 1) fig.append_trace({ 'x': data['Longitude'], 'y': data['Latitude'], 'text': data['time'], 'name': 'Longitude vs Latitude', 'mode': 'lines+markers', 'type': 'scatter' }, 2, 1) return fig if __name__ == '__main__': app.run_server(debug=True)页面加载的更新默认情况下，Dash 应用程序会将 app.layout 存储在内存中。这可确保布局只在应用程序启动时计算一次。如果您将 app.layout 设置为函数，那么您可以在每次页面加载时提供动态布局。例如，如果您的 app.layout 看起来像这样。import datetime import dash import dash_html_components as html app.layout = html.H1('The time is: ' + str(datetime.datetime.now())) if __name__ == '__main__': app.run_server(debug=True)那么你的应用程序将显示应用程序启动时的时间。如果你把它改为函数，那么每次刷新页面时都会计算出一个新的日期时间。试试吧。提高性能缓存由于 Dash 的回调在本质上是函数式的（它们不包含任何状态），所以很容易添加 memoization 缓存。Memoization 会在函数被调用后存储它的结果，并且如果用相同的参数调用该函数，则会重新使用该结果。Dash 应用程序经常跨多个进程或线程部署。在这些情况下，每个进程或线程都包含自己的内存，它不会跨实例共享内存。这意味着，如果我们要使用 lru_cache，我们的缓存结果可能不会在各个会话之间共享。取而代之的是，我们可以使用 Flask-Caching 库，它可以将结果保存在共享内存数据库中，比如 Redis，或者作为文件系统中的文件。Flask-Caching 还有其他不错的功能，比如基于时间的过期。如果你想每小时或每天更新数据（清除缓存），基于时间的过期是很有用的。下面是一个使用 Redis 的 Flask-Caching 的例子。import datetime import os import dash import dash_core_components as dcc import dash_html_components as html from dash.dependencies import Input, Output from flask_caching import Cache external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css'] app = dash.Dash(__name__, external_stylesheets=external_stylesheets) cache = Cache(app.server, config={ # try 'filesystem' if you don't want to setup redis 'CACHE_TYPE': 'redis', 'CACHE_REDIS_URL': os.environ.get('REDIS_URL', '') }) app.config.suppress_callback_exceptions = True timeout = 20 app.layout = html.Div([ html.Div(id='flask-cache-memoized-children'), dcc.RadioItems( id='flask-cache-memoized-dropdown', options=[ {'label': 'Option {}'.format(i), 'value': 'Option {}'.format(i)} for i in range(1, 4) ], value='Option 1' ), html.Div('Results are cached for {} seconds'.format(timeout)) ]) @app.callback( Output('flask-cache-memoized-children', 'children'), [Input('flask-cache-memoized-dropdown', 'value')]) @cache.memoize(timeout=timeout) # in seconds def render(value): return 'Selected "{}" at "{}"'.format( value, datetime.datetime.now().strftime('%H:%M:%S') ) if __name__ == '__main__': app.run_server(debug=True)这里是一个缓存数据集而不是回调的例子。它使用 FileSystem 缓存，将缓存的结果保存到文件系统中。如果有一个数据集被用来更新几个回调，这种方法很好用。import datetime as dt import os import time import dash import dash_core_components as dcc import dash_html_components as html import numpy as np import pandas as pd from dash.dependencies import Input, Output from flask_caching import Cache external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css'] app = dash.Dash(__name__, external_stylesheets=external_stylesheets) cache = Cache(app.server, config={ 'CACHE_TYPE': 'filesystem', 'CACHE_DIR': 'cache-directory' }) TIMEOUT = 60 @cache.memoize(timeout=TIMEOUT) def query_data(): # This could be an expensive data querying step df = pd.DataFrame( np.random.randint(0,100,size=(100, 4)), columns=list('ABCD') ) now = dt.datetime.now() df['time'] = [now - dt.timedelta(seconds=5*i) for i in range(100)] return df.to_json(date_format='iso', orient='split') def dataframe(): return pd.read_json(query_data(), orient='split') app.layout = html.Div([ html.Div('Data was updated within the last {} seconds'.format(TIMEOUT)), dcc.Dropdown( id='live-dropdown', value='A', options=[{'label': i, 'value': i} for i in dataframe().columns] ), dcc.Graph(id='live-graph') ]) @app.callback(Output('live-graph', 'figure'), [Input('live-dropdown', 'value')]) def update_live_graph(value): df = dataframe() now = dt.datetime.now() return { 'data': [{ 'x': df['time'], 'y': df[value], 'line': { 'width': 1, 'color': '#0074D9', 'shape': 'spline' } }], 'layout': { # display the current position of now # this line will be between 0 and 60 seconds # away from the last datapoint 'shapes': [{ 'type': 'line', 'xref': 'x', 'x0': now, 'x1': now, 'yref': 'paper', 'y0': 0, 'y1': 1, 'line': {'color': 'darkgrey', 'width': 1} }], 'annotations': [{ 'showarrow': False, 'xref': 'x', 'x': now, 'xanchor': 'right', 'yref': 'paper', 'y': 0.95, 'yanchor': 'top', 'text': 'Current time ({}:{}:{})'.format( now.hour, now.minute, now.second), 'bgcolor': 'rgba(255, 255, 255, 0.8)' }], # aesthetic options 'margin': {'l': 40, 'b': 40, 'r': 20, 't': 10}, 'xaxis': {'showgrid': False, 'zeroline': False}, 'yaxis': {'showgrid': False, 'zeroline': False} } } if __name__ == '__main__': app.run_server(debug=True)
前言之前也写过一些关于可视化的文章，比如：[post cid="43" /][post cid="84" /]不过都是比较基础的入门内容。最近在学量化，有位开发者整理了常见的量化图表，原文章，都是用matplotlib和seaborn做的。我个人还是更倾向于颜值高又可交互的plotly的，所以把他的代码改写为plotly。大多数能改成功，也有一些我不知道怎么实现的，只能搁置了，若有大神知道代码怎么写，欢迎评论区留言。前置首先导入相关库，并作相应的设置import numpy as np import pandas as pd import plotly.express as px import plotly.graph_objects as go from plotly.subplots import make_subplots import plotly.figure_factory as ff import matplotlib.pyplot as plt import plotly plotly.offline.init_notebook_mode(connected=True) # 这个是作图的初始设定，因为我嫌默认的字体太小了，所以改得大了些 layout = dict(title_x=0.5, # title居中 font=dict(family="Times New Roman",size=20,color="RebeccaPurple"), # 字体设置 coloraxis_colorbar=dict(xanchor="left",x=0.75,ticks="outside"), # 颜色条设置 margin=dict(b= 40,l=40, r=40, t= 40) # 大小设置 )数据仍然使用原作者提供的美股5家上市公司日线数据，我手动下载下来又传到github上了，大家使用的时候可以下载原数据，或者直接读取远程连接。github地址：https://github.com/caly5144/shu-s-project/tree/master/quan/visual下面就是获取数据的代码，主要有两种，默认是只使用特斯拉的日线数据，后面作相关性热力图的时候会用到第二种数据，这里提前把数据整理了一下，由长格式数据转为宽格式数据。def get_date(is_tesla=1): df = pd.read_excel('data.xlsx') # 也可以直接读取下面的远程连接，无需再手动下载数据了 #df = pd.read_csv('https://cdn.jsdelivr.net/gh/caly5144/shu-s-project/quan/visual/data.csv') if is_tesla: df = df[df['code']=='usTSLA'] df = df.reset_index() else: df = df'time', 'code', 'p_change' df = df.pivot_table(index=["time"],columns=["code"],values=["p_change"]) df.columns = df.columns.droplevel(0) df.index.name = None df = df.reset_index().dropna() df = df.rename(columns={'index':'time'}) print(df) return df作图价格与均线首先是最简单的折线图，一般用于展示股票价格变化代码def plot(): # 普通折线图 df = get_date() fig = px.line(df, x="time", y="close",title='tsla close').update_layout(layout) plotly.offline.plot(fig)效果 当然也可以同时展示多个指标或股票，加个color=参数即可。def price(): # 价格与均线 df = get_date() for window in [30,60,90]: df[str(window)+' mv'] = df['close'].rolling(window).mean() df = df.melt(id_vars='time',value_vars=['close', '30 mv','60 mv','90 mv']) fig = px.line(df, x="time", y="value", color="variable",labels={"variable": "指标"})\ .update_layout(layout) fig.update_yaxes(visible=False, showticklabels=False) plotly.offline.plot(fig) 蜡烛图（K线图）蜡烛图，也称K线图，是作技术分析时最常见的图def candle(): # 蜡烛图 df = get_date() df['ma_5'] = df['close'].rolling(5).mean() # 计算5日均线 #为了示例清晰，只拿出后30天的交易数据绘制蜡烛图 df = df[-30:] fig = go.Figure(data=[go.Candlestick(x=df['time'], open=df['open'],high=df['high'], low=df['low'],close=df['close'], increasing_line_color= 'red', # 上升是红色 decreasing_line_color= 'green' # 下降是绿色 ,name = 'K线图')]).update_layout(layout) #fig.update_layout(xaxis_rangeslider_visible=False) # 底部时间拖动条是否可见 fig.add_trace(go.Scatter(x=df.time, y=df.ma_5,name="5日均线")) plotly.offline.plot(fig) 量价图大家看股票的时候，除了关心价格外，还关心成交量，下面的代码就给出了如何将K线图与成交量展示在同一张图中。def can_vol():# 量价图 df = get_date() df['ma_30'] = df['close'].rolling(30).mean() fig = make_subplots(rows=2, cols=1, shared_xaxes=True) # shared_xaxes是否共享x轴 fig.append_trace({"type": "candlestick", "x": df['time'], "low": df['low'],"high": df['high'],"open": df['open'], "close": df['close'],"increasing_line_color": "red", "decreasing_line_color":'green','name':'K线'},1,1) fig.append_trace({'x':df.time,'y':df.ma_30,'type':'scatter','name':'30日均线'},1,1) fig.append_trace({'x':df.time,'y':df.volume,'type':'bar','name':'成交量'},2,1) fig['layout']['yaxis1'].update(domain=[0.2, 1]) fig['layout']['yaxis2'].update(domain=[0, 0.2]) # 上面代码用于调整两幅图的顺序以及各自大小（宽相同，高不同） fig.update_layout(layout) fig.update_layout(xaxis_rangeslider_visible=False) # 上方拖动条不可见 fig.update_layout(xaxis2_rangeslider_visible=True) # 展示最下方的时间拖动条 #fig.update_layout(showlegend=True) plotly.offline.plot(fig) 有些同学说，我想让它们放到一张图中，而不是以子图的形式展现，怎么做？def candle_vol(): # 量价图 df = get_date() df['ma_5'] = df['close'].rolling(5).mean() trace1 = { "name":"K线图","type": "candlestick", "x": df['time'],"yaxis": "y2", "low": df['low'],"high": df['high'], "open": df['open'],"close": df['close'], "decreasing": {"line": {"color": "red"}}, "increasing": {"line": {"color": "green"}} } trace2 = { "name": "成交量", "type": "bar", "x": df.time,"y": df.volume,"yaxis": "y", "marker": {"color":"#6495ED"} } data = [trace1, trace2] layout1 = { "yaxis": {"domain": [0, 0.2], "showticklabels": False}, "yaxis2": {"domain": [0.2, 0.8]}, "legend": { "x": 1, "y": 0.8 }, } layout.update(layout1) fig = go.Figure(data=data, layout=layout) plotly.offline.plot(fig) 收益率与波动率图由四个子图构成，列出了常见的收益率与标准差def returns(): # 收益率与波动图 df = get_date() # 投资回报 df['return'] = np.log(df['close'] / df['close'].shift(1)) # 移动收益标准差 df['mov_std'] = df['return'].rolling(20).std()* np.sqrt(20) # 加权移动收益标准差，与移动收益标准差基本相同，只不过根据时间权重计算std df['std_ewm'] = df['return'].ewm(span=20,min_periods=20, adjust=True).std()* np.sqrt(20) fig = make_subplots(rows=4, cols=1) alist = ['close', 'return','mov_std','std_ewm'] for item in alist: fig.add_trace( go.Scatter(x=df['time'], y=df[item],name=item), row=alist.index(item)+1, col=1) fig.update_layout(layout) plotly.offline.plot(fig) 多类型子图这幅图主要展示的是低开高走的交易日的下一个交易日的股票价格变化（我没明白这有什么用……），其实内容都一样，都是频率分布图，但我没办法在plotly上实现，所以直接照抄原作者的代码，用matplotlib了。def subplot(): # 多类型子图 df = get_date() # iloc获取所有低开高走的交易日的下一个交易日组成low_to_high_df，由于是下一个交易日 # 所以要对满足条件的交易日再次通过iloc获取，下一个交易日index用key.values + 1 # key序列的值即为0-len(tsla_df), 即为交易日index low_to_high_df = df.iloc[df[(df.close > df.open) & (df.key != df.shape[0] - 1)].key.values + 1] # 通过where将下一个交易日的涨跌幅通过ceil，floor向上，向下取整 change_ceil_floor = np.where(low_to_high_df['p_change'] > 0, np.ceil(low_to_high_df['p_change']), np.floor(low_to_high_df['p_change'])) # 使用pd.Series包裹，方便之后绘制 change_ceil_floor = pd.Series(change_ceil_floor) _, axs = plt.subplots(nrows=2, ncols=2, figsize=(12, 10)) # 竖直柱状图，可以看到-1的柱子最高, 图5-7左上 change_ceil_floor.value_counts().plot(kind='bar', ax=axs[0][0]) # 水平柱状图，可以看到-1的柱子最长, 图5-7右上 change_ceil_floor.value_counts().plot(kind='barh', ax=axs[0][1]) # 概率密度图，可以看到向左偏移, 图5-7左下 change_ceil_floor.value_counts().plot(kind='kde', ax=axs[1][0]) # 圆饼图，可以看到－1所占的比例最高, -2的比例也大于＋2，图5-7右下 change_ceil_floor.value_counts().plot(kind='pie', ax=axs[1][1]) PS：我尝试了单独画频率分布直方图，但它没有办法像图中那样按高度降序排列，网上的资料说.update_xaxes(categoryorder="total descending")即可，但对我的图无效。箱型图这张图展示了每周5个交易日的价格变化范围def box(): # 箱型图 df = get_date() fig = px.box(df, x="date_week", y="p_change", notched=False,color = 'date_week', boxmode="overlay").update_layout(layout) fig.update_layout(showlegend=False) plotly.offline.plot(fig) 相关性热力图该图展示了几只股票的相关性def heat(): # 相关性热力图 df = get_date(is_tesla=0) df = df.set_index('time') corr = df.corr() index_list = corr.index.tolist() col_list = corr.columns.values.tolist() fig = px.imshow(corr,x = col_list,y = index_list,color_continuous_scale='blugrn').update_layout(layout) plotly.offline.plot(fig) 卖出原因这幅图展示了股票持有期（绿色），以及卖出的原因def sell_reseaon(buy_date,sell_date): # 卖出原因 df = get_date() # 找出2014-07-28对应时间序列中的index作为start start = df[df.time == buy_date].key.values[0] # 找出2014-09-05对应时间序列中的index作为end end = df[df.time == sell_date].key.values[0]+1 fig = px.line(df, x="time", y="close",color_discrete_map = {"close": "red"}).update_layout(layout) fig.add_trace(go.Scatter(x=df.time, y=df.close, fill='tozeroy',fillcolor='rgba(0,0,255,0.08)')) fig.add_trace(go.Scatter(x=df.time[start:end], y=df.close[start:end], fill='tozeroy',fillcolor='rgba(0,255,0,0.38)')) #fill='tozeroy'是填充值x轴，tonexty是填充至另外一个trace，fillcolor是填充颜色，因为red,green之类的没法设定透明度，只能用rgba了 fig.update_layout(showlegend=False,annotations=[ dict(x='2014-09-05',y=df.close[end-1], xref="x",yref="y",text="sell for stop loss", showarrow=True,arrowhead=7,ax=0,ay=-40 )]) plotly.offline.plot(fig) 黄金分割线这张图展示了几条黄金分割线def gold(): # 黄金分割线 from scipy import stats df = get_date() cs_max = df.close.max() # 收盘价格序列中的最小值 cs_min = df.close.min() sp382 = (cs_max - cs_min) * 0.382 + cs_min sp618 = (cs_max - cs_min) * 0.618 + cs_min print('视觉上的382: ' + str(round(sp382, 2))) print('视觉上的618: ' + str(round(sp618, 2))) sp382_stats = stats.scoreatpercentile(df.close, 38.2) sp618_stats = stats.scoreatpercentile(df.close, 61.8) print('统计上的382: ' + str(round(sp382_stats, 2))) print('统计上的618: ' + str(round(sp618_stats, 2))) # 从视觉618和统计618中筛选更大的值 above618 = np.maximum(sp618, sp618_stats) # 从视觉618和统计618中筛选更小的值 below618 = np.minimum(sp618, sp618_stats) # 从视觉382和统计382中筛选更大的值 above382 = np.maximum(sp382, sp382_stats) # 从视觉382和统计382中筛选更小的值 below382 = np.minimum(sp382, sp382_stats) df['above618'] = above618 df['below618'] = below618 df['above382'] = above382 df['below382'] = below382 df = df.melt(id_vars='time',value_vars=['close', 'above618','below618','above382', 'below382'],var_name='指标') fig = px.line(df, x="time", y="value",color='指标').update_layout(layout) plotly.offline.plot(fig)
利用python进行数据收集、整理、分析和绘图的全过程
这是github上的一个项目，作为数据分析基础入门是非常好的，于是我把它译为中文，希望对大家有所帮助。原文地址代码均已在本地测试通过。要求在此存储库中，我将记录收集数据，处理数据并进行可视化的完整过程。该项目中使用的数据集是国家数据-社会保障卡应用中心的婴儿名字，其中包括1880年至2018年的记录。需要安装python3与以下库：Requests - 下载数据集.pandas - 进行数据分析.NumPy - 矩阵运算.Matplotlib - 绘图.seaborn - 美化图.获取数据import requests url = "https://www.ssa.gov/oact/babynames/names.zip" with requests.get(url) as response: with open("names.zip", "wb") as temp_file: temp_file.write(response.content)提示：如果因为网速等问题下载失败，可手动复制链接到浏览器中下载。这就将数据的zip包下载到我们电脑中了，它包括了各年份数据的txt文件与一个说明pdf文档，我们需要做的是读取txt文件，进行处理并保存到csv中。这个新的csv文件将成为我们的数据集，并将包含4个字段（年份，名称，性别和数量）import csv from zipfile import ZipFile # 这两个都是内置模块，无需安装 data_list = "year", "name", "gender", "count" # 此列表将保存我们的所有数据。我们使用标题行对其进行初始化。 with ZipFile("names.zip") as temp_zip: # 读取zip对象 for file_name in temp_zip.namelist(): # 读取文件列表 if ".txt" in file_name: # 只读取txt文档 with temp_zip.open(file_name) as temp_file: # 从压缩包中读取这个txt for line in temp_file.read().decode("utf-8").splitlines(): # 文件以二进制形式打开，我们使用utf-8对其进行解码，以便可以将其作为字符串进行操作。 # 准备好我们所需的数据字段并将其添加到列表中. line_chunks = line.split(",") year = file_name[3:7] name = line_chunks[0] gender = line_chunks[1] count = line_chunks[2] data_list.append([year, name, gender, count]) # 保存成csv csv.writer(open("data.csv", "w", newline="", encoding="utf-8")).writerows(data_list)首先我们声明一个data_list，它是一个二维数组。目的是为了稍后我们可以用csv.writer.writerows()方法保存到csv中。（我更喜欢用writerows()而不是writerow()，因为它更快）接下来的步骤都有注释，就不再赘述了。数据探索（Data Exploration）基础信息首先导入相关库import matplotlib.pyplot as plt import numpy as np import pandas as pd import seaborn as sns接着我们使用pandas.read_csv()方法读取csvdf = pd.read_csv("data.csv") # 由于我们的数据集非常简单，因此我们不需要设置任何特殊标志或额外参数，默认值就足够了。让我们从经典的df.head()和df.tail()方法开始，他们分别能查看数据集前几行和后几行。df.head() # 前五行 >>> year name gender count 0 1880 Mary F 7065 1 1880 Anna F 2604 2 1880 Emma F 2003 3 1880 Elizabeth F 1939 4 1880 Minnie F 1746 df.tail() # 后五行 >>> year name gender count 1957041 2018 Zylas M 5 1957042 2018 Zyran M 5 1957043 2018 Zyrie M 5 1957044 2018 Zyron M 5 1957045 2018 Zzyzx M 5这些样本告诉我们以下信息：有4列（年份，名称，性别和计数）。有1,957,046行。行按年份排序。女性记录显示在男性记录之前。2018年，至少有5个父母将其儿子命名为``Zzyzx''。唯一名称df["name"].nunique() # 拥有独一无二名字的人有多少 df[df["gender"] == "M"]["name"].nunique() # 女性拥有独一无二名字的人有多少 df[df["gender"] == "F"]["name"].nunique() # 男性拥有独一无二名字的人有多少 # 性别中立的人有多少拥有独一无二的名字 both_df = df.pivot_table(index="name", columns="gender", values="count", aggfunc=np.sum).dropna() both_df.index.nunique()可以看出，数据集中有98,400个人拥有唯一名称。其中，男性41,475名，女性67,698名，性别中立10,773名。十大常用名字为了获得前10个最常用的男性和女性名字，我们将首先用dataframe按性别过滤。有了特定性别dataframe后，我们将只选择2个字段，即姓名和人数。然后在name字段上使用groupby()方法，并使用sum()汇总结果。最后，我们将对count字段上的值进行降序排序，并使用head(10)方法获得前10个结果。df = df[df["gender"] == "M"] df = df"name", "count" df = df.groupby("name") df = df.sum() df = df.sort_values("count", ascending=False) df.head(10) >>> name count James 5164280 John 5124817 Robert 4820129 Michael 4362731 William 4117369 David 3621322 Joseph 2613304 Richard 2565301 Charles 2392779 Thomas 2311849上面的代码可以写到一起，即：df[df["gender"] == "M"]"name", "count".groupby("name").sum().sort_values("count", ascending=False).head(10)当然，把"gender"改成"F"就可以找女性十大常用名了。前二十大性别中立者常用名这有点挑战，首先我们需要旋转dataframe，以便名称成为索引，性别成为列，所有计数的总和（每个名称，每个性别）将成为我们的值。我们将分步骤进行。首先，我们旋转表并删除值为0的行。即名称不在男性或女性中的行。df = df.pivot_table(index="name", columns="gender", values="count", aggfunc=np.sum).dropna()会输出类似的数据：gender F M name Aaden 5.0 4828.0 Aadi 16.0 851.0 Aadyn 16.0 516.0 Aalijah 149.0 212.0 Aaliyah 87442.0 96.0 ... ...有了这种形状的数据，我们现在知道每个姓名每个性别有多少条记录。现在，我们仅考虑那些至少具有50,000个性别记录的名称。df = df[(df["M"] >= 50000) & (df["F"] >= 50000)] df.head(20)注意：原本我只使用前10个名字，但由于只有20个符合我们标准的名字，我将其推到了20个。姓名记录最高和最低的年份现在，我们将知道哪个年份的记录之和最高和最低。both_df = df.groupby("year").sum() male_df = df[df["gender"] == "M"].groupby("year").sum() female_df = df[df["gender"] == "F"].groupby("year").sum() # 组合最小值（年份和计数） both_df.min()["count"] both_df.idxmin()["count"] # 男性最小值（年份和计数） male_df.min()["count"] male_df.idxmin()["count"] # Female Min (count and year) female_df.min()["count"] female_df.idxmin()["count"] # Combined Max (count and year) both_df.max()["count"] both_df.idxmax()["count"] # Male Max (count and year) male_df.max()["count"] male_df.idxmax()["count"] # Female Max (count and year) female_df.max()["count"] female_df.idxmax()["count"]绘图在此项目中，我们仅使用线绘图，这对于显示值随时间的变化非常有帮助。首先要做的是设置一些自定义颜色，这些颜色将全局应用于每个图。# 这些参数生成带有淡紫色的图。 sns.set(style="ticks", rc={ "figure.figsize": [12, 7], "text.color": "white", "axes.labelcolor": "white", "axes.edgecolor": "white", "xtick.color": "white", "ytick.color": "white", "axes.facecolor": "#443941", "figure.facecolor": "#443941"} )声明样式后，我们就可以绘制数据了。为了效率，接下来我会使用单线，但是不用担心，我会解释每条线的作用。按年份计数我们第一个线绘图将展示历年姓名记录数据的变化。首先，为男性、女性、组合创建新的dataframesboth_df = df.groupby("year").sum() male_df = df[df["gender"] == "M"].groupby("year").sum() female_df = df[df["gender"] == "F"].groupby("year").sum()我们直接绘制dataframes。索引为x轴，总数为y轴。plt.plot(both_df, label="Both", color="yellow") plt.plot(male_df, label="Male", color="lightblue") plt.plot(female_df, label="Female", color="pink")设置y轴的间距为500,000，首先我们格式化标签的数据，然后使用实际数据作为间隔。yticks_labels = ["{:,}".format(i) for i in range(0, 4500000+1, 500000)] plt.yticks(np.arange(0, 4500000+1, 500000), yticks_labels)添加一些其他设置项plt.legend() plt.grid(False) plt.xlabel("Year") plt.ylabel("Records Count") plt.title("Records per Year") plt.show() 最受欢迎名字的增长情况首先，我们合并男性和女性的值，然后旋转表，让名称是索引，年份是列。我们还用零填充缺失值。pivoted_df = df.pivot_table( index="name", columns="year", values="count", aggfunc=np.sum).fillna(0)然后按年份计算每个名字的百分比。percentage_df = pivoted_df / pivoted_df.sum() * 100增加新的一列作为百分比之和percentage_df["total"] = percentage_df.sum(axis=1)我们对dataframe进行排序以检查哪些是最高值并将进行切片。之后，我们删除total列，因为将不再使用它。sorted_df = percentage_df.sort_values( by="total", ascending=False).drop("total", axis=1)[0:10]翻转轴，以便更轻易地绘图transposed_df = sorted_df.transpose()我们通过使用列名作为标签和Y轴来单独地绘制每个名字。for name in transposed_df.columns.tolist(): plt.plot(transposed_df.index, transposed_df[name], label=name)设置y轴间距为0.5%yticks_labels = ["{}%".format(i) for i in np.arange(0, 5.5, 0.5)] plt.yticks(np.arange(0, 5.5, 0.5), yticks_labels)增加最后的自定义项plt.legend() plt.grid(False) plt.xlabel("Year") plt.ylabel("Percentage by Year") plt.title("Top 10 Names Growth") plt.show() 十大热门名字我们将获得过去10年（2008-2018）的最热门的十个名字变化趋势和上面的思路类似，直接上代码# 首先要做的是删除所有早于2008年的记录。 filtered_df = df[df["year"] >= 2008] # 然后，我们合并男性女性值，并对表进行数据透视，因此名称是我们的索引，年份是我们的列。我们还用零填充缺失值。 pivoted_df = filtered_df.pivot_table( index="name", columns="year", values="count", aggfunc=np.sum).fillna(0) # 然后我们按年份计算每个名称的百分比。 percentage_df = pivoted_df / pivoted_df.sum() * 100 # 我们添加了一个新列来表示百分比之和。 percentage_df["total"] = percentage_df.sum(axis=1) # 我们对dataframe进行排序以检查哪些是最高值并将进行切片。之后，我们删除total列，因为将不再使用它。 sorted_df = percentage_df.sort_values( by="total", ascending=False).drop("total", axis=1)[0:10] # 翻转轴，以便更轻松地绘制数据框。 transposed_df = sorted_df.transpose() # 我们通过使用列名作为标签和Y轴来单独地绘制每个名字。 for name in transposed_df.columns.tolist(): plt.plot(transposed_df.index, transposed_df[name], label=name) # Y轴0.5%的间距 yticks_labels = ["{:.2f}%".format(i) for i in np.arange(0.3, 0.7, 0.05)] plt.yticks(np.arange(0.3, 0.7, 0.05), yticks_labels) # 从2008年到2018年，我们将x轴的间距设为1 xticks_labels = ["{}".format(i) for i in range(2008, 2618+1, 1)] plt.xticks(np.arange(2008, 2018+1, 1), xticks_labels) # 最后设置 plt.legend() plt.grid(False) plt.xlabel("Year") plt.ylabel("Percentage by Year") plt.title("Top 10 Trending Names") plt.show()