├── lib ├── __init__.py ├── utils.py ├── errors.py ├── life.py ├── chain.py ├── regexp.py ├── mapping.py ├── result.py ├── check.py ├── formatter.py ├── complement.py ├── painter.py └── answer.py ├── data ├── tail │ └── Is.txt ├── question │ ├── Value.txt │ ├── When.txt │ └── Exist.txt ├── reference │ ├── Max.txt │ ├── Index.txt │ ├── Catalog.txt │ ├── Location.txt │ ├── Status.txt │ ├── ChildIndex.txt │ └── ParentIndex.txt ├── raw.7z └── data.json ├── .gitattributes ├── .gitignore ├── doc ├── web-1.png ├── web-2.png ├── web-3.png ├── graph-1.png ├── graph-2.png ├── struct.png ├── graph-info.png ├── graph-locate.png ├── graph-value.png ├── graph-contain.png ├── graph-include.png └── graph-indexname.png ├── web ├── main │ ├── __init__.py │ └── views.py ├── templates │ ├── nb_components.html │ ├── components.html │ ├── nb_nteract.html │ ├── simple_chart.html │ ├── nb_jupyter_lab.html │ ├── nb_jupyter_lab_tab.html │ ├── nb_jupyter_notebook.html │ ├── nb_jupyter_notebook_tab.html │ ├── simple_tab.html │ ├── simple_page.html │ ├── index.html │ ├── simple_globe.html │ ├── nb_jupyter_globe.html │ └── macro ├── __init__.py └── static │ ├── style │ └── index.css │ └── js │ └── func.js ├── run_cmd.py ├── run_web.py ├── const.py ├── requirements.txt ├── test ├── errors_test.py ├── parser_test.py ├── classifier_test.py └── answer_test.py ├── LICENSE ├── chatbot.py ├── README.md ├── question_parser.py ├── demo ├── demo1.ipynb ├── demo3.ipynb └── demo2.ipynb ├── question_classifier.py └── answer_search.py /lib/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /data/tail/Is.txt: -------------------------------------------------------------------------------- 1 | 是 2 | 有 3 | 为 4 | 是? 5 | 有? 6 | 为? -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | demo/*.ipynb linguist-detectable=true 2 | -------------------------------------------------------------------------------- /data/question/Value.txt: -------------------------------------------------------------------------------- 1 | 是多少 2 | 有多少 3 | 为多少 4 | 值为 5 | 值是 6 | 的值 -------------------------------------------------------------------------------- /data/reference/Max.txt: -------------------------------------------------------------------------------- 1 | 最高 2 | 最低 3 | 最大 4 | 最小 5 | 最多 6 | 最少 7 | 极大 8 | 极小 -------------------------------------------------------------------------------- /data/question/When.txt: -------------------------------------------------------------------------------- 1 | 哪年 2 | 哪一年 3 | 何时 4 | 何年 5 | 什么时 6 | 什么年 7 | 几时 8 | 几年 -------------------------------------------------------------------------------- /data/reference/Index.txt: -------------------------------------------------------------------------------- 1 | 指标 2 | 索引 3 | 靶标 4 | 目标 5 | 配额 6 | 指示 7 | 指针 8 | 标志 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | data/raw 2 | .idea 3 | data/dicts 4 | results 5 | demo/.ipynb_checkpoints -------------------------------------------------------------------------------- /data/raw.7z: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shawnh2/QA-CivilAviationKG/HEAD/data/raw.7z -------------------------------------------------------------------------------- /doc/web-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shawnh2/QA-CivilAviationKG/HEAD/doc/web-1.png -------------------------------------------------------------------------------- /doc/web-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shawnh2/QA-CivilAviationKG/HEAD/doc/web-2.png -------------------------------------------------------------------------------- /doc/web-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shawnh2/QA-CivilAviationKG/HEAD/doc/web-3.png -------------------------------------------------------------------------------- /data/data.json: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shawnh2/QA-CivilAviationKG/HEAD/data/data.json -------------------------------------------------------------------------------- /doc/graph-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shawnh2/QA-CivilAviationKG/HEAD/doc/graph-1.png -------------------------------------------------------------------------------- /doc/graph-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shawnh2/QA-CivilAviationKG/HEAD/doc/graph-2.png -------------------------------------------------------------------------------- /doc/struct.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shawnh2/QA-CivilAviationKG/HEAD/doc/struct.png -------------------------------------------------------------------------------- /doc/graph-info.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shawnh2/QA-CivilAviationKG/HEAD/doc/graph-info.png -------------------------------------------------------------------------------- /doc/graph-locate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shawnh2/QA-CivilAviationKG/HEAD/doc/graph-locate.png -------------------------------------------------------------------------------- /doc/graph-value.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shawnh2/QA-CivilAviationKG/HEAD/doc/graph-value.png -------------------------------------------------------------------------------- /doc/graph-contain.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shawnh2/QA-CivilAviationKG/HEAD/doc/graph-contain.png -------------------------------------------------------------------------------- /doc/graph-include.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shawnh2/QA-CivilAviationKG/HEAD/doc/graph-include.png -------------------------------------------------------------------------------- /doc/graph-indexname.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shawnh2/QA-CivilAviationKG/HEAD/doc/graph-indexname.png -------------------------------------------------------------------------------- /data/reference/Catalog.txt: -------------------------------------------------------------------------------- 1 | 目录 2 | 标准 3 | 基准 4 | 基线 5 | 准则 6 | 规格 7 | 规范 8 | 规章 9 | 定额 10 | 原则 11 | 条例 12 | 常规 -------------------------------------------------------------------------------- /data/reference/Location.txt: -------------------------------------------------------------------------------- 1 | 各地 2 | 各区 3 | 组成地 4 | 组成区 5 | 各范围 6 | 各行政领域 7 | 每地 8 | 每区 9 | 每个地 10 | 每个区 11 | 所有地 12 | 所有区 -------------------------------------------------------------------------------- /web/main/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint 2 | 3 | main = Blueprint('main', __name__) 4 | 5 | from . import views 6 | -------------------------------------------------------------------------------- /data/reference/Status.txt: -------------------------------------------------------------------------------- 1 | 总述 2 | 总体 3 | 总状 4 | 趋势 5 | 情况 6 | 情形 7 | 形势 8 | 状况 9 | 状态 10 | 实况 11 | 现况 12 | 发展 13 | 整体 14 | 如何 -------------------------------------------------------------------------------- /run_cmd.py: -------------------------------------------------------------------------------- 1 | from chatbot import CAChatBot 2 | 3 | if __name__ == '__main__': 4 | chatbot = CAChatBot(mode='cmd') 5 | chatbot.run() 6 | -------------------------------------------------------------------------------- /run_web.py: -------------------------------------------------------------------------------- 1 | from const import DEBUG 2 | from web import create_app 3 | 4 | 5 | if __name__ == '__main__': 6 | app = create_app() 7 | app.run(debug=DEBUG) 8 | -------------------------------------------------------------------------------- /web/templates/nb_components.html: -------------------------------------------------------------------------------- 1 | {% import 'macro' as macro %} 2 | 3 | {% for chart in charts %} 4 | {{ macro.gen_components_content(chart) }} 5 | {% endfor %} 6 | -------------------------------------------------------------------------------- /const.py: -------------------------------------------------------------------------------- 1 | URI = 'http://localhost:7474' 2 | USERNAME = 'neo4j' 3 | PASSWORD = 'shawn' 4 | DEBUG = True 5 | # DEBUG = False 6 | 7 | CHART_RENDER_DIR = 'results' # 生成图表的保存位置 8 | -------------------------------------------------------------------------------- /data/reference/ChildIndex.txt: -------------------------------------------------------------------------------- 1 | 组成 2 | 组合 3 | 子集 4 | 子指标 5 | 子级 6 | 子孙 7 | 子嗣 8 | 嗣子 9 | 子部份 10 | 子部分 11 | 子类 12 | 儿子 13 | 孩子 14 | 构成 15 | 成分 16 | 成份 17 | 集成 18 | 后裔 19 | 后嗣 20 | 后代 -------------------------------------------------------------------------------- /web/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | 3 | from .main import main as main_blueprint 4 | 5 | 6 | def create_app(): 7 | app = Flask(__name__) 8 | app.register_blueprint(main_blueprint) 9 | 10 | return app 11 | -------------------------------------------------------------------------------- /data/question/Exist.txt: -------------------------------------------------------------------------------- 1 | 有什么 2 | 有些什么 3 | 有啥 4 | 有哪些 5 | 有甚 6 | 有些啥 7 | 有what 8 | 都有 9 | 存在什么 10 | 存在些什么 11 | 存在啥 12 | 存在哪些 13 | 存在甚 14 | 存在些啥 15 | 存在what 16 | 都存在 17 | 含什么 18 | 含些什么 19 | 含啥 20 | 含哪些 21 | 含甚 22 | 含些啥 23 | 含what 24 | 都含 -------------------------------------------------------------------------------- /data/reference/ParentIndex.txt: -------------------------------------------------------------------------------- 1 | 父指标 2 | 父级 3 | 母指标 4 | 母级 5 | 父母 6 | 双亲 7 | 总指标 8 | 总数 9 | 总级 10 | 总和 11 | 总额 12 | 总计 13 | 总的 14 | 总体 15 | 总量 16 | 总值 17 | 总比 18 | 合计 19 | 上级 20 | 上一级 21 | 全指标 22 | 全数 23 | 全级 24 | 全体 25 | 全部 26 | 整体 27 | 整个 28 | 整额 29 | 整级 30 | 集合 -------------------------------------------------------------------------------- /web/templates/components.html: -------------------------------------------------------------------------------- 1 | {% import 'macro' as macro %} 2 | 3 | 4 | 5 | 6 | {{ chart.page_title }} 7 | 8 | 9 | 10 | {{ macro.gen_components_content(chart) }} 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /web/templates/nb_nteract.html: -------------------------------------------------------------------------------- 1 | {% import 'macro' as macro %} 2 | 3 | 4 | 5 | 6 | {{ macro.render_chart_dependencies(chart) }} 7 | 8 | 9 | {% for c in chart %} 10 | {{ macro.render_chart_content(c) }} 11 | {% endfor %} 12 | 13 | 14 | -------------------------------------------------------------------------------- /web/templates/simple_chart.html: -------------------------------------------------------------------------------- 1 | {% import 'macro' as macro %} 2 | 3 | 4 | 5 | 6 | {{ chart.page_title }} 7 | {{ macro.render_chart_dependencies(chart) }} 8 | 9 | 10 | {{ macro.render_chart_content(chart) }} 11 | 12 | 13 | -------------------------------------------------------------------------------- /web/templates/nb_jupyter_lab.html: -------------------------------------------------------------------------------- 1 | {% import 'macro' as macro %} 2 | 3 | 4 | 5 | 6 | 7 | 8 | {% for chart in charts %} 9 | {% if chart._component_type in ("table", "image") %} 10 | {{ macro.gen_components_content(chart) }} 11 | {% else %} 12 | {{ macro.render_chart_content(chart) }} 13 | {% endif %} 14 | {% endfor %} 15 | 16 | 17 | -------------------------------------------------------------------------------- /web/templates/nb_jupyter_lab_tab.html: -------------------------------------------------------------------------------- 1 | {% import 'macro' as macro %} 2 | 3 | 4 | 5 | 6 | 7 | 8 | {{ macro.generate_tab_css() }} 9 | {{ macro.display_tablinks(charts) }} 10 | 11 | {% for chart in charts %} 12 | {% if chart._component_type in ("table", "image") %} 13 | {{ macro.gen_components_content(chart) }} 14 | {% else %} 15 | {{ macro.render_chart_content(chart) }} 16 | {% endif %} 17 | {% endfor %} 18 | {{ macro.switch_tabs() }} 19 | 20 | 21 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | certifi==2020.12.5 2 | Click==7.0 3 | colorama==0.4.4 4 | Flask==1.1.2 5 | importlib-metadata==3.7.2 6 | itsdangerous==1.1.0 7 | Jinja2==2.11.3 8 | MarkupSafe==1.1.1 9 | neobolt==1.7.17 10 | neotime==1.7.4 11 | prettytable==2.1.0 12 | prompt-toolkit==2.0.10 13 | py2neo==4.3.0 14 | pyahocorasick==1.4.0 15 | pyecharts==1.9.0 16 | Pygments==2.3.1 17 | #python-Levenshtein 18 | pytz==2021.1 19 | simplejson==3.17.2 20 | six==1.15.0 21 | typing-extensions==3.7.4.3 22 | urllib3==1.24.3 23 | wcwidth==0.2.5 24 | Werkzeug==1.0.1 25 | wincertstore==0.2 26 | zipp==3.4.1 27 | -------------------------------------------------------------------------------- /web/templates/nb_jupyter_notebook.html: -------------------------------------------------------------------------------- 1 | {% import 'macro' as macro %} 2 | 3 | 10 | 11 | {% for chart in charts %} 12 | {% if chart._component_type in ("table", "image") %} 13 | {{ macro.gen_components_content(chart) }} 14 | {% else %} 15 |
16 | {% endif %} 17 | {% endfor %} 18 | 19 | {{ macro.render_notebook_charts(charts, libraries) }} 20 | -------------------------------------------------------------------------------- /test/errors_test.py: -------------------------------------------------------------------------------- 1 | import os 2 | import unittest 3 | 4 | from run_cmd import CAChatBot 5 | 6 | os.chdir(os.path.join(os.getcwd(), '..')) 7 | 8 | 9 | class QCErrTest(unittest.TestCase): 10 | bot = CAChatBot() 11 | 12 | def query(self, question: str): 13 | return self.bot.query(question) 14 | 15 | def test_order_err(self): 16 | self.assertEqual(self.query('11年总量是港澳台运输总周转量的多少倍?'), '不明白你所指的“总量”。是问反了吗?') 17 | 18 | def test_overstep_err(self): 19 | self.assertEqual(self.query('11年游客周转量同比增长?'), '年报中并未记录“2010”年的数据!') 20 | 21 | 22 | if __name__ == '__main__': 23 | unittest.main() 24 | -------------------------------------------------------------------------------- /lib/utils.py: -------------------------------------------------------------------------------- 1 | from const import DEBUG 2 | 3 | 4 | def read_words(path: str) -> list: 5 | # 加载词汇 6 | with open(path, encoding='utf-8') as f: 7 | return [w.strip('\n') for w in f] 8 | 9 | 10 | def write_to_file(filepath: str, lines: list): 11 | # 保存关键字词典到本地 12 | with open(filepath, 'w', encoding='utf-8') as f: 13 | for line in lines: 14 | f.write(line + '\n') 15 | 16 | 17 | def sign(n: float, repr_: tuple = ('少', '多')): 18 | """ 以字符串的形式返回浮点数的正负号 """ 19 | return repr_[0] if n < 0 else repr_[1] 20 | 21 | 22 | def debug(*args): 23 | if DEBUG: 24 | print('DEBUG:', *args) 25 | -------------------------------------------------------------------------------- /web/templates/nb_jupyter_notebook_tab.html: -------------------------------------------------------------------------------- 1 | {% import 'macro' as macro %} 2 | 3 | 10 | 11 | {{ macro.generate_tab_css() }} 12 | {{ macro.display_tablinks(charts) }} 13 | 14 | {% for chart in charts %} 15 | {% if chart._component_type in ("table", "image") %} 16 | {{ macro.gen_components_content(chart) }} 17 | {% else %} 18 |
19 | {% endif %} 20 | {% endfor %} 21 | 22 | {{ macro.render_notebook_charts(charts, libraries) }} 23 | {{ macro.switch_tabs() }} 24 | -------------------------------------------------------------------------------- /lib/errors.py: -------------------------------------------------------------------------------- 1 | # 问题的有关错误 2 | from lib.check import check_contain 3 | 4 | 5 | class QuestionError(Exception): 6 | """ 问题错误 """ 7 | pass 8 | 9 | 10 | class QuestionOrderError(QuestionError): 11 | """ 问句的顺序颠倒错误。 12 | 例.11年总量是港澳台运输总周转量的多少倍? 【错误】 13 | 11年港澳台运输总周转量是总量的多少倍? 【正确】 14 | """ 15 | @staticmethod 16 | def check(results: list, parent_words: list): 17 | sent = results[0][0] 18 | if check_contain(parent_words, sent): 19 | raise QuestionOrderError(f'不明白你所指的“{sent}”。是问反了吗?') 20 | 21 | 22 | class QuestionYearOverstep(QuestionError): 23 | """ 问句中涉及的年份越界。支持年份为2011-2019年。""" 24 | @staticmethod 25 | def check(year: int): 26 | if year > 2019 or year < 2011: 27 | raise QuestionYearOverstep(f'年报中并未记录“{year}”年的数据!') 28 | -------------------------------------------------------------------------------- /lib/life.py: -------------------------------------------------------------------------------- 1 | # 为知识图谱结构关系编码/解码生命周期 2 | 3 | 4 | class Life: 5 | 6 | def __init__(self): 7 | self._mask = 0x01 8 | self._code = {} # 保存关键字的编码值 9 | 10 | def encode(self, obj: str): 11 | """ 编码,若已存在则不再进行编码 """ 12 | if self._code.get(obj) is None: 13 | self._code[obj] = self._mask 14 | self._mask *= 2 15 | 16 | def get_life(self, obj: str): 17 | """ 返回编码,若不存在则返回0 """ 18 | code = self._code.get(obj) 19 | return code if code is not None else 0 20 | 21 | @staticmethod 22 | def live(year, life) -> bool: 23 | """ 返回输入year编码是否还存在生命 """ 24 | return (year & life) != 0 25 | 26 | @staticmethod 27 | def extend_life(life, code): 28 | """ 延续生命周期,code为延续时长编码 """ 29 | return life + code 30 | -------------------------------------------------------------------------------- /lib/chain.py: -------------------------------------------------------------------------------- 1 | # 解析器翻译链 2 | 3 | 4 | class TranslationChain: 5 | """ 按照sql语句的执行顺序串成链, 后链需前一链的结果作为输入({}为其占位符)。""" 6 | 7 | def __init__(self): 8 | self._chain = {} 9 | self._offset = 0 10 | 11 | def make(self, sqls: list): 12 | self._chain[self._offset] = sqls 13 | return self 14 | 15 | def then(self, sqls: list): 16 | self._offset += 1 17 | return self.make(sqls) 18 | 19 | def reset(self): 20 | """ 重置目前的链 """ 21 | self._chain.clear() 22 | self._offset = 0 23 | 24 | def iter(self, offset: int = 0, unpack: bool = False): 25 | gen = self._chain[offset][0] if unpack else self._chain[offset] 26 | for sql in gen: 27 | yield sql 28 | 29 | def __repr__(self): 30 | return str(self._chain) 31 | -------------------------------------------------------------------------------- /web/main/views.py: -------------------------------------------------------------------------------- 1 | from flask import render_template, request, jsonify 2 | 3 | from . import main 4 | from chatbot import CAChatBot 5 | 6 | chatbot = CAChatBot(mode='web') 7 | temp_charts = [] 8 | 9 | 10 | @main.route('/') 11 | def index(): 12 | return render_template('index.html') 13 | 14 | 15 | @main.route('/send', methods=['GET', 'POST']) 16 | def send_answer(): 17 | global temp_charts 18 | 19 | question = request.values.get('question') 20 | answers = chatbot.query(question) 21 | charts = [] 22 | if len(answers) == 2: 23 | answers, charts = answers 24 | temp_charts = charts 25 | return jsonify({'answer': answers, 'chart_count': len(charts)}) 26 | 27 | 28 | @main.route('/chart', methods=['GET', 'POST']) 29 | def send_chart(): 30 | global temp_charts 31 | 32 | i = int(request.values.get('chart_index')) 33 | return temp_charts[i].dump_options() 34 | -------------------------------------------------------------------------------- /web/templates/simple_tab.html: -------------------------------------------------------------------------------- 1 | {% import 'macro' as macro %} 2 | 3 | 4 | 5 | 6 | {{ chart.page_title }} 7 | {{ macro.render_chart_dependencies(chart) }} 8 | {{ macro.render_chart_css(chart) }} 9 | 10 | 11 | {{ macro.generate_tab_css() }} 12 | {{ macro.display_tablinks(chart) }} 13 | 14 |
15 | {% for c in chart %} 16 | {% if c._component_type in ("table", "image") %} 17 | {{ macro.gen_components_content(c) }} 18 | {% else %} 19 | {{ macro.render_chart_content(c) }} 20 | {% endif %} 21 | {% endfor %} 22 |
23 | 24 | 29 | {{ macro.switch_tabs() }} 30 | 31 | 32 | -------------------------------------------------------------------------------- /web/templates/simple_page.html: -------------------------------------------------------------------------------- 1 | {% import 'macro' as macro %} 2 | 3 | 4 | 5 | 6 | {{ chart.page_title }} 7 | {{ macro.render_chart_dependencies(chart) }} 8 | {{ macro.render_chart_css(chart) }} 9 | 10 | 11 | 12 | {% if chart.download_button %} 13 | 14 | {% endif %} 15 |
16 | {% for c in chart %} 17 | {% if c._component_type in ("table", "image") %} 18 | {{ macro.gen_components_content(c) }} 19 | {% else %} 20 | {{ macro.render_chart_content(c) }} 21 | {% endif %} 22 | {% for _ in range(chart.page_interval) %}
{% endfor %} 23 | {% endfor %} 24 |
25 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /web/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 民航公报咨询室 6 | 7 | 8 | 9 | 10 | 11 |
12 |
13 |
14 |
15 |
16 | 20 |
21 |
22 | 23 | 24 |
25 |
26 | 27 | 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 ShawnHu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /lib/regexp.py: -------------------------------------------------------------------------------- 1 | # 存放相关正则表达式 2 | __all__ = ['MultipleCmp1', 'MultipleCmp2', 3 | 'NumberCmp1', 'NumberCmp2', 4 | 'GrowthCmp', 5 | 'RangeYear', 'RefsYear'] 6 | 7 | # 值的倍数关系比较 8 | MultipleCmp1 = r'[\d]+年*的*([\D]+)(占[有据]*|是|为)([\D]+)' 9 | MultipleCmp2 = r'[\d]+年*的*([\D]*)(?:占[有据]*|是|为)[\d]+年*的*([\D]+)' 10 | # 值的多少关系比较 11 | NumberChange = r'(?:多(?!少)|(? 3 | 4 | 5 | 6 | {{ chart.page_title }} 7 | {{ macro.render_chart_dependencies(chart) }} 8 | 9 | 10 |
11 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /web/templates/nb_jupyter_globe.html: -------------------------------------------------------------------------------- 1 | 8 | 9 | {% for chart in charts %} 10 |
11 | {% endfor %} 12 | 13 | 14 | 46 | -------------------------------------------------------------------------------- /lib/mapping.py: -------------------------------------------------------------------------------- 1 | # 存放相关映射 2 | from operator import mul, add 3 | 4 | # 前缀-标签映射字典 5 | PREFIX_LABEL_MAP = { 6 | 'Y': "Year", # 年份 7 | 'C': "Catalog", # 目录 8 | 'I': "Index", # 指标 9 | 'A': "Area" # 地区/机场/公司集团 10 | } 11 | # 前缀-结构关系映射字典 12 | PREFIX_S_REL_MAP = {'Y-C': "include", 'C-I': "include", 'I-I': "contain", 13 | 'I-A': "locate", 'A-A': "contain"} 14 | # 前缀-值关系映射字典 15 | PREFIX_V_REL_MAP = {'Y-C': "info", 'Y-I': "value", 'Y-A': None} 16 | 17 | # 数字字符映射 18 | Char2CharDigit = {'零': '0', '一': '1', '二': '2', '三': '3', '四': '4', '五': '5', 19 | '六': '6', '七': '7', '八': '8', '九': '9', '两': '2', '千': '', '十': ''} 20 | Char2Digit = {'零': 0, '一': 1, '二': 2, '三': 3, '四': 4, '五': 5, 21 | '六': 6, '七': 7, '八': 8, '九': 9, '两': 2, '千': 0, '十': 10, 22 | '1': 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, '8': 8, '9': 9, '0': 10} 23 | # 指代字符映射 24 | Ref2Digit = {'去': -1, '上': -1, '前': -2, '大': -1} 25 | 26 | 27 | def map_digits(year: str) -> str: 28 | new_year = year 29 | # 替换 30 | for k, v in Char2CharDigit.items(): 31 | new_year = new_year.replace(k, v) 32 | # 填充 33 | if len(new_year) == 2: 34 | new_year = '20' + new_year 35 | return new_year 36 | 37 | 38 | def map_refs(year: str, num: int, base_year: int) -> str: 39 | n = 0 40 | map_dict = Ref2Digit 41 | operator = add 42 | if num in (2, 3): 43 | n = -1 if num == 2 else 1 44 | map_dict = Char2Digit 45 | operator = mul 46 | 47 | for ch in year: 48 | d = map_dict.get(ch) 49 | if d: 50 | n = operator(n, d) 51 | if num == 3: 52 | return ','.join([str(base_year - i) for i in range(1, n + 1)]) 53 | else: 54 | return str(base_year + n) 55 | -------------------------------------------------------------------------------- /lib/result.py: -------------------------------------------------------------------------------- 1 | from lib.chain import TranslationChain 2 | 3 | 4 | class Result(object): 5 | """ 保存查询使用的信息 """ 6 | def __init__(self, region_wds: dict, raw_q: str, filtered_q: str): 7 | # 特征词 8 | self.region_wds = region_wds # word: type_ 9 | self.region_wds_types = [t for t in self.region_wds.values()] 10 | self.region_wds_reverse = self.reverse_region_dict() # type_: word 11 | # 问题 12 | self.raw_question = raw_q 13 | self.filtered_question = filtered_q 14 | # 一些结果 15 | self.question_types = [] 16 | self.sqls = {} 17 | 18 | def __getitem__(self, type_name: str): 19 | return self.region_wds_reverse.get(type_name) 20 | 21 | def __len__(self): 22 | return len(self.region_wds) 23 | 24 | def __contains__(self, type_name: str): 25 | return type_name in self.region_wds_types 26 | 27 | def is_wds_null(self): 28 | return self.region_wds == {} 29 | 30 | def is_qt_null(self): 31 | return self.question_types == [] 32 | 33 | def count(self, type_name: str = None): 34 | """返回某个特征词类型的个数""" 35 | return self.region_wds_types.count(type_name) 36 | 37 | def add_word(self, word: str, type_: str): 38 | if word not in self.region_wds.keys(): 39 | self.region_wds[word] = type_ 40 | self.region_wds_types.append(type_) 41 | self.region_wds_reverse.setdefault(type_, []).append(word) 42 | 43 | def add_qtype(self, question_type): 44 | self.question_types.append(question_type) 45 | 46 | def add_sql(self, qt: str, sqls: TranslationChain): 47 | self.sqls[qt] = sqls 48 | 49 | def reverse_region_dict(self): 50 | # 转换值为键 51 | new_dict = {} 52 | for k, v in self.region_wds.items(): 53 | new_dict.setdefault(v, []).append(k) 54 | return new_dict 55 | 56 | def replace_words(self, old_word: str, new_word: str): 57 | """ 更改句子中的词语 """ 58 | self.filtered_question = self.filtered_question.replace(old_word, new_word) 59 | -------------------------------------------------------------------------------- /chatbot.py: -------------------------------------------------------------------------------- 1 | from question_classifier import QuestionClassifier 2 | from question_parser import QuestionParser 3 | from answer_search import AnswerSearcher 4 | from lib.errors import QuestionError 5 | 6 | 7 | class CAChatBot: 8 | 9 | def __init__(self, mode: str = 'cmd'): 10 | assert mode in ('cmd', 'notebook', 'web') 11 | 12 | self.classifier = QuestionClassifier() 13 | self.parser = QuestionParser() 14 | self.searcher = AnswerSearcher() 15 | 16 | self.mode = mode 17 | 18 | print("欢迎与小航对话,请问有什么可以帮助您的?") 19 | 20 | self.default_answer = '抱歉!小航能力有限,无法回答您这个问题。可以联系开发者哟!' 21 | self.goodbye = '小航期待与你的下次见面,拜拜!' 22 | 23 | def query(self, question: str): 24 | try: 25 | final_ans = '' 26 | # 开始查询 27 | result = self.classifier.classify(question) 28 | if result is None or result.is_qt_null(): 29 | return self.default_answer 30 | result = self.parser.parse(result) 31 | answers = self.searcher.search(result) 32 | # 合并回答与渲染图表 33 | for answer in answers: 34 | final_ans += (answer.to_string().rstrip('。') + '。') 35 | if answer.have_charts() and self.mode != 'web': 36 | answer.combine_charts() 37 | answer.render_chart(result.raw_question) 38 | # 依不同模式返回 39 | if self.mode == 'notebook': 40 | return final_ans, answers[0].get_chart() # None or chart 41 | elif self.mode == 'web': 42 | return final_ans, answers[0].get_charts() # chart list 43 | else: # default: 'cmd' 44 | return final_ans 45 | except QuestionError as err: 46 | return err.args[0] 47 | 48 | def run(self): 49 | while 1: 50 | question = input('[我]: ') 51 | if question.lower() == 'q': 52 | print(self.goodbye) 53 | break 54 | answer = self.query(question) 55 | print('[小航]: ', answer) 56 | -------------------------------------------------------------------------------- /lib/check.py: -------------------------------------------------------------------------------- 1 | # 问题的检查 2 | import re 3 | from types import FunctionType 4 | 5 | __all__ = ['check_contain', 'check_all_contain', 'check_list_contain', 'check_list_any_contain', 6 | 'check_regexp', 'check_endswith'] 7 | 8 | 9 | def check_contain(words: list, question: str) -> bool: 10 | """检查是否有包含关系""" 11 | for word in words: 12 | if word in question: 13 | return True 14 | return False 15 | 16 | 17 | def check_all_contain(words: list, question: str) -> bool: 18 | """检查是否全部包含""" 19 | for word in words: 20 | if word not in question: 21 | return False 22 | return True 23 | 24 | 25 | def check_list_contain(words: list, dst: list, *pos: int, not_: int = None) -> bool: 26 | """检查列表中指定位置的包含关系, 有一个不存在即为假, not_即不能存在的位置""" 27 | for i in pos: 28 | if not check_contain(words, dst[i]): 29 | return False 30 | if not_ and check_contain(words, dst[not_]): 31 | return False 32 | return True 33 | 34 | 35 | def check_list_any_contain(words: list, dst: list, *pos: int) -> bool: 36 | """检查列表中指定位置的包含关系, 有一个存在即为真""" 37 | for i in pos: 38 | if check_contain(words, dst[i]): 39 | return True 40 | return False 41 | 42 | 43 | def check_endswith(words: list, question: str) -> bool: 44 | """检查尾部关系""" 45 | return question.endswith(tuple(words)) 46 | 47 | 48 | def check_regexp(question: str, *patterns: str, functions: list, callback: FunctionType = None) -> bool: 49 | """ 检查正则关系 50 | 51 | :param question: 问题 52 | :param patterns: 正则表达式 53 | :param functions: 为每个正则表达式的匹配结果调用 54 | :param callback: 在function调用后结果为假时使用 55 | :return: 假值 或 真值 56 | """ 57 | for pattern, function in zip(patterns, functions): 58 | results = re.compile(pattern).findall(question) 59 | if results: 60 | value = function(results) 61 | if not value: 62 | if callback: 63 | callback(results) 64 | return False 65 | else: 66 | return True 67 | return False 68 | -------------------------------------------------------------------------------- /lib/formatter.py: -------------------------------------------------------------------------------- 1 | # 格式化sql查询结果 2 | import pickle 3 | 4 | 5 | class Formatter: 6 | 7 | def __init__(self, data): 8 | self._life_path = './data/dicts/life.pk' 9 | # flags 10 | self._is_none = (data is None or len(data) == 0) 11 | # fields 12 | self.name = '' # index_name 13 | self.area = '' 14 | self.info = '' 15 | self.value = '' 16 | self.unit = '' 17 | self.life = '' 18 | self.repr = '' 19 | self.label = '' 20 | self.child_id = '' 21 | # init 22 | self._distribute(data) 23 | 24 | def _distribute(self, data): 25 | """ 将传入的数据字段分散为各个字段 """ 26 | if self._is_none: 27 | return 28 | for k, v in data.items(): 29 | if k.endswith('name'): 30 | self.name = v 31 | elif k.endswith('area'): 32 | self.area = v 33 | elif k.endswith('info'): 34 | self.info = v 35 | elif k.endswith('value'): 36 | self.value = v 37 | elif k.endswith('unit'): 38 | self.unit = v 39 | elif k.endswith('life'): 40 | self.life = int(v) 41 | elif k.endswith('repr'): 42 | self.repr = v 43 | elif k.startswith('label'): 44 | self.label = v 45 | elif k.endswith('child_id'): 46 | self.child_id = v 47 | 48 | def __repr__(self): 49 | return f'' 51 | 52 | def __bool__(self): 53 | return not self._is_none 54 | 55 | def life_check(self, year: str): 56 | """ 检查并剔除不在生命周期中的数据 """ 57 | if self._is_none: 58 | return 59 | with open(self._life_path, 'rb') as f: 60 | life = pickle.load(f) 61 | year = life.get_life(year) 62 | if life.live(year, self.life) == 0: 63 | self._is_none = True 64 | 65 | def subject(self): 66 | """ 获取主语 """ 67 | return f'{self.area}{self.repr}{self.name}' 68 | 69 | def val(self): 70 | """ 获取取值 """ 71 | return f'{self.value}{self.unit}' 72 | -------------------------------------------------------------------------------- /lib/complement.py: -------------------------------------------------------------------------------- 1 | # 问题的填充 2 | import re 3 | 4 | import Levenshtein 5 | 6 | from lib.utils import read_words 7 | from lib.regexp import RangeYear, RefsYear 8 | from lib.mapping import map_digits, map_refs 9 | 10 | 11 | def year_complement(question: str) -> str: 12 | """ 年份自动填充,转换各种表示为数字表示。 13 | 例:11年 -> 2011年 14 | 两千一十一年 -> 2011年 15 | 11-15年 -> 2011年,2012年,2013年,2014年,2015年 16 | 13到15年 -> 2013年,2014年,2015年 17 | 18 | 13年比前年 -> 2013年比2011年 19 | 15年比大大前年 -> 2015年比2011年 20 | 21 | 16年比3年前 -> 2016年比2013年 22 | 16年与前三年相比 -> 2016年与2015年,2014年,2013年相比 23 | """ 24 | complemented = question 25 | 26 | # 先填充范围 27 | range_years = re.compile(RangeYear).findall(question) 28 | last_year = '' 29 | for (year, gap) in range_years: 30 | year = year.strip('年') 31 | if not gap: 32 | new_year = map_digits(year) 33 | else: 34 | start, end = year.split(gap) 35 | start_year, end_year = int(map_digits(start)), int(map_digits(end)) 36 | new_year = ','.join([str(start_year + i) for i in range(end_year - start_year + 1)]) 37 | last_year = new_year 38 | complemented = complemented.replace(year, new_year) 39 | 40 | # 后填充指代 41 | for i, pattern in enumerate(RefsYear): 42 | ref_years = re.compile(pattern).findall(complemented) 43 | if ref_years: 44 | year = ref_years[0][-1] 45 | new_year = map_refs(year, i, int(last_year)) 46 | complemented = complemented.replace(year, new_year) 47 | break 48 | 49 | return complemented 50 | 51 | 52 | def index_complement(question: str, words: list, 53 | len_threshold: int = 4, 54 | ratio_threshold: float = 0.5) -> tuple: 55 | """对问题中的指标名词进行模糊查询并迭代返回最接近的项. 56 | 57 | :param question: 问题 58 | :param words: 查询范围(词集) 59 | :param len_threshold: 最小的有效匹配长度 60 | :param ratio_threshold: 最小匹配率 61 | :return: 首次匹配结果 62 | """ 63 | charset = read_words('./data/dicts/fast_index_table.txt')[0] 64 | pattern = re.compile(f'([{charset}]+)') 65 | 66 | for result in pattern.findall(question): 67 | if len(result) < len_threshold or result in words: 68 | continue 69 | scores = [] 70 | for word in words: 71 | score = Levenshtein.ratio(word, result) 72 | scores.append(score) 73 | # 得分最高的最近似 74 | max_score = max(scores) 75 | if max_score >= ratio_threshold: 76 | return words[scores.index(max_score)], result 77 | return None, None 78 | -------------------------------------------------------------------------------- /web/static/style/index.css: -------------------------------------------------------------------------------- 1 | html { 2 | height: 100%; 3 | } 4 | 5 | body { 6 | height: 100%; 7 | margin: 0; 8 | } 9 | 10 | #chatroom { 11 | height: 100%; 12 | } 13 | 14 | #output-area { 15 | background-color: #f5f5f5; 16 | height: 70%; 17 | overflow-y: auto; 18 | } 19 | 20 | #input-area { 21 | background-color: #ffffff; 22 | height: 25%; 23 | } 24 | 25 | #buttons-area { 26 | height: 5%; 27 | background-color: #ffffff; 28 | } 29 | 30 | #buttons-area button { 31 | outline: medium; 32 | height: 30px; 33 | width: 7%; 34 | float: right; 35 | margin-right: 20px; 36 | font-weight: bold; 37 | border-radius: 10px; 38 | border: 2px solid transparent; 39 | transition: border 0.25s ease-in; 40 | } 41 | 42 | #buttons-area button:hover { 43 | border: 2px solid gray; 44 | transition: border 0.25s ease-out; 45 | } 46 | 47 | #input-dialog { 48 | width: 100%; 49 | height: 100%; 50 | border: none; 51 | padding: 15px; 52 | outline: medium; 53 | resize: none; 54 | font-size: 16px; 55 | box-sizing: border-box; 56 | } 57 | 58 | .question { 59 | background-color: white; 60 | text-align: left; 61 | font-size: 17px; 62 | padding: 10px 15px; 63 | height: 30px; 64 | line-height: 30px; 65 | margin: 15px; 66 | border-radius: 10px; 67 | box-shadow: -4px 7px 20px -6px rgba(0, 0, 0, .1); 68 | } 69 | 70 | .answer { 71 | text-align: left; 72 | padding: 10px 15px; 73 | margin: 15px; 74 | } 75 | 76 | .answer-chart { 77 | width: 70%; 78 | height: 500px; 79 | padding: 10px 15px; 80 | } 81 | 82 | .answer-error { 83 | text-align: left; 84 | padding: 10px 15px; 85 | margin: 15px; 86 | background-color: crimson; 87 | color: white; 88 | border-radius: 10px; 89 | } 90 | 91 | /*tooltip*/ 92 | .tooltip { 93 | position: relative; 94 | display: inline-block; 95 | white-space: nowrap; 96 | border-bottom: 1px dotted #777; 97 | } 98 | 99 | .tooltip-content { 100 | opacity: 0; 101 | visibility: hidden; 102 | font: 12px Arial, Helvetica; 103 | text-align: center; 104 | border-color: #aaa #555 #555 #aaa; 105 | border-style: solid; 106 | border-width: 1px; 107 | padding: 15px; 108 | position: absolute; 109 | bottom: 40px; 110 | left: 50%; 111 | margin-left: -76px; 112 | background-color: #fff; 113 | transition: bottom .2s ease, opacity .2s ease; 114 | } 115 | 116 | .tooltip-content:after, 117 | .tooltip-content:before { 118 | bottom: -15px; 119 | content: ""; 120 | position: absolute; 121 | left: 50%; 122 | margin-left: -10px; 123 | } 124 | 125 | .tooltip-content:before { 126 | border-right-width: 25px; 127 | border-top-color: #555; 128 | border-top-width: 15px; 129 | bottom: -15px; 130 | } 131 | 132 | .tooltip:hover .tooltip-content{ 133 | opacity: 1; 134 | visibility: visible; 135 | bottom: 30px; 136 | } -------------------------------------------------------------------------------- /lib/painter.py: -------------------------------------------------------------------------------- 1 | # 图表绘制器 2 | import os 3 | 4 | from pyecharts.charts import Bar, Pie, Line 5 | from pyecharts import options as opts 6 | from pyecharts.globals import ThemeType 7 | 8 | from const import CHART_RENDER_DIR 9 | 10 | 11 | class Painter: 12 | 13 | def __init__(self): 14 | if not os.path.exists(CHART_RENDER_DIR): 15 | os.mkdir(CHART_RENDER_DIR) 16 | 17 | def paint_bar(self, x: list, collects: list, title: str, mark_point: bool = False): 18 | bar = Bar(init_opts=opts.InitOpts(theme=ThemeType.LIGHT)) 19 | bar.add_xaxis(x) 20 | for collect in collects: 21 | for i, (name, unit, data) in enumerate(collect): 22 | bar.add_yaxis(f'{name}-单位:{unit}', data, yaxis_index=i) 23 | if i != 0: 24 | bar.extend_axis( 25 | yaxis=opts.AxisOpts( 26 | name='', 27 | type_='value', 28 | position='right', 29 | ) 30 | ) 31 | bar.set_global_opts( 32 | title_opts=opts.TitleOpts( 33 | title=title, 34 | pos_left='5%' 35 | ), 36 | legend_opts=opts.LegendOpts(pos_bottom='0') 37 | ) 38 | bar.set_series_opts( 39 | label_opts=opts.LabelOpts(position='top'), 40 | tooltip_opts=opts.TooltipOpts(formatter=f'{{b}}年{{a}}:{{c}}') 41 | ) 42 | if mark_point: 43 | bar.set_series_opts( 44 | markpoint_opts=opts.MarkPointOpts( 45 | data=[ 46 | opts.MarkPointItem(type_='max', name='最大值'), 47 | opts.MarkPointItem(type_='min', name='最小值') 48 | ], 49 | symbol_size=80 50 | ) 51 | ) 52 | return bar 53 | 54 | def paint_pie(self, data_pairs: list, units: list, title: str, sub_titles: list): 55 | old_i = i = 10 56 | j = 60 57 | for data_pair, unit, sub_title in zip(data_pairs, units, sub_titles): 58 | pie = Pie(init_opts=opts.InitOpts(height='300px', width='auto')) 59 | for pairs in data_pair.values(): 60 | pie.add( 61 | unit, pairs, 62 | radius=[60, 80], center=[f'{i}%', f'{j}%'] 63 | ) 64 | i += 20 65 | pie.set_global_opts( 66 | title_opts=opts.TitleOpts( 67 | title=title, 68 | pos_left='5%', 69 | subtitle=sub_title 70 | ), 71 | legend_opts=opts.LegendOpts( 72 | pos_top='20%', 73 | pos_left='5%' 74 | ) 75 | ) 76 | pie.set_series_opts( 77 | label_opts=opts.LabelOpts(is_show=False), 78 | tooltip_opts=opts.TooltipOpts(formatter='{b}:{c}{a}({d}%)') 79 | ) 80 | i = old_i 81 | yield pie 82 | 83 | def paint_bar_stack_with_line(self, x: list, children: dict, parents: dict, sub_title: str): 84 | for (parent_name, unit), item in children.items(): 85 | bar = Bar(init_opts=opts.InitOpts(theme=ThemeType.MACARONS)) 86 | bar.add_xaxis(x) 87 | line = Line() 88 | line.add_xaxis(x) 89 | child_names = [] 90 | for child_name, data, overall in item: 91 | bar.add_yaxis(child_name, data, stack='stack1') 92 | line.add_yaxis(f'{child_name}占比', overall, yaxis_index=1) 93 | child_names.append(child_name) 94 | bar.add_yaxis( 95 | parent_name, 96 | parents[parent_name], 97 | stack='stack1', 98 | yaxis_index=0 99 | ) 100 | bar.set_global_opts( 101 | title_opts=opts.TitleOpts( 102 | title=','.join(child_names), 103 | subtitle=sub_title, 104 | pos_left='5%' 105 | ), 106 | legend_opts=opts.LegendOpts(pos_bottom='0') 107 | ) 108 | bar.set_series_opts( 109 | label_opts=opts.LabelOpts(is_show=False), 110 | tooltip_opts=opts.TooltipOpts(formatter=f'{{b}}年{{a}}:{{c}}{unit}'), 111 | itemstyle_opts=opts.ItemStyleOpts(opacity=0.5) 112 | ) 113 | bar.extend_axis( 114 | yaxis=opts.AxisOpts( 115 | type_='value', 116 | name='所占比例', 117 | min_=0, 118 | max_=1, 119 | position='right', 120 | splitline_opts=opts.SplitLineOpts( 121 | is_show=True, 122 | linestyle_opts=opts.LineStyleOpts(opacity=1) 123 | ) 124 | ) 125 | ) 126 | bar.overlap(line) 127 | yield bar 128 | 129 | def paint_line(self, x: list, tag: str, y: list, title: str): 130 | line = Line() 131 | line.add_xaxis(x) 132 | line.add_yaxis(tag, y) 133 | line.set_global_opts(title_opts=opts.TitleOpts(title=title)) 134 | return line 135 | -------------------------------------------------------------------------------- /test/parser_test.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from question_classifier import QuestionClassifier 4 | from question_parser import QuestionParser 5 | 6 | os.chdir(os.path.join(os.getcwd(), '..')) 7 | 8 | 9 | class QPTest: 10 | qc = QuestionClassifier() 11 | qp = QuestionParser() 12 | 13 | def parse(self, question: str): 14 | res = self.qc.classify(question) 15 | if res.is_qt_null(): 16 | print('[err]', question) 17 | else: 18 | print(self.qp.parse(res).sqls) 19 | 20 | def test(self): 21 | self.test_year_status() 22 | self.test_catalog_status() 23 | self.test_exist_catalog() 24 | self.test_index_overall() 25 | self.test_area_overall() 26 | self.test_index_compose() 27 | self.test_indexes_2mn_compare() 28 | self.test_areas_2mn_compare() 29 | self.test_indexes_g_compare() 30 | self.test_areas_g_compare() 31 | self.test_index_2_overall() 32 | self.test_area_2_overall() 33 | self.test_indexes_trend() 34 | self.test_areas_trend() 35 | 36 | def test_year_status(self): 37 | self.parse('2011年总体情况怎样?') 38 | self.parse('2011年发展形势怎样?') 39 | self.parse('2011年发展如何?') 40 | self.parse('11年形势怎样?') 41 | 42 | def test_catalog_status(self): 43 | self.parse('2011年运输航空总体情况怎样?') 44 | self.parse('2011年航空安全发展形势怎样?') 45 | self.parse('2011年教育及科技发展如何?') 46 | self.parse('2011固定资产投资形势怎样?') 47 | 48 | def test_exist_catalog(self): 49 | self.parse('2011年有哪些指标目录?') 50 | self.parse('2011年有哪些基准?') 51 | self.parse('2011年有啥规格?') 52 | self.parse('2011年的目录有哪些?') 53 | 54 | def test_index_overall(self): 55 | self.parse('2011年的游客周转量占总体多少?') 56 | self.parse('2011年的游客周转量占父指标多少份额?') 57 | self.parse('2011年的游客周转量是总体的多少倍?') 58 | self.parse('2011游客周转量占总体的百分之多少?') 59 | self.parse('2011年的游客周转量为其总体的多少倍?') 60 | self.parse('2011年游客周转量占有总额的多少比例?') 61 | self.parse('2011游客周转量占总量的多少?') 62 | 63 | def test_area_overall(self): 64 | self.parse('11年国内的运输总周转量占总体的百分之几?') 65 | self.parse('11年国际运输总周转量占总值的多少?') 66 | self.parse('11年港澳台运输总周转量是全体的多少倍?') 67 | 68 | def test_index_compose(self): 69 | self.parse('2011年游客周转量的子集有?') 70 | self.parse('2011年游客周转量的组成?') 71 | self.parse('2011年游客周转量的子指标组成情况?') 72 | 73 | def test_indexes_2mn_compare(self): 74 | self.parse('2011年游客周转量是12年的百分之几?') 75 | self.parse('2011年的是12年游客周转量的百分之几?') 76 | self.parse('2011年游客周转量占12年的百分之?') 77 | self.parse('2011年游客周转量是12年的几倍?') 78 | self.parse('2011年游客周转量比12年降低了?') 79 | self.parse('12年的货邮周转量比去年变化了多少?') 80 | self.parse('2012年游客周转量比去年多了多少?') 81 | self.parse('12年的货邮周转量同去年相比变化了多少?') 82 | self.parse('2011年游客周转量和货邮周转量为12年的多少倍?') 83 | 84 | def test_areas_2mn_compare(self): 85 | self.parse('11年港澳台运输总周转量是12年的多少倍?') 86 | self.parse('12年的是11年港澳台运输总周转量的多少倍?') 87 | self.parse('12年港澳台运输总周转量占11年百分之几?') 88 | self.parse('12年港澳台运输总周转量和游客周转量是11年比例?') 89 | self.parse('2011年国内游客周转量比一二年多多少?') 90 | self.parse('2012年港澳台游客周转量比上一年的少多少?') 91 | self.parse('2011年港澳台与国内的游客周转量相比12降低多少?') 92 | self.parse('2011年港澳台的游客周转量同2012相比降低多少?') 93 | 94 | def test_indexes_g_compare(self): 95 | self.parse('2012年游客周转量同比增长多少?') 96 | self.parse('2012年游客周转量同比下降百分之几?') 97 | self.parse('2012年游客周转量和货邮周转量同比下降百分之几?') 98 | 99 | def test_areas_g_compare(self): 100 | self.parse('2012年国内游客周转量同比增长了?') 101 | self.parse('2012年国内游客周转量同比下降了多少?') 102 | self.parse('2012年国内游客周转量和货邮周转量同比变化了多少?') 103 | 104 | def test_index_2_overall(self): 105 | self.parse('2012年游客周转量占总体的百分比比去年变化多少?') 106 | self.parse('2012年游客周转量占总体的百分比,相比11年变化多少?') 107 | self.parse('2012年相比11年,游客周转量占总体的百分比变化多少?') 108 | self.parse('2012年的游客周转量占总计比例比去年增加多少?') 109 | self.parse('2013年的游客周转量占父级的倍数比11年降低多少?') 110 | 111 | def test_area_2_overall(self): 112 | self.parse('2012年国内的游客周转量占总体的百分比比去年变化多少?') 113 | self.parse('2012年国际游客周转量占总体的百分比,相比11年变化多少?') 114 | self.parse('2012年相比11年,港澳台游客周转量占总体的百分比变化多少?') 115 | self.parse('2012年的国内游客周转量占总计比例比去年增加多少?') 116 | self.parse('2013年的国际游客周转量占父级的倍数比11年降低多少?') 117 | self.parse('2013年的国际和国内游客周转量占父级的倍数比11年降低多少?') 118 | 119 | def test_indexes_trend(self): 120 | self.parse('2011-13年运输总周转量的变化趋势如何?') 121 | self.parse('2011-13年运输总周转量情况?') 122 | self.parse('2011-13年运输总周转量值分布状况?') 123 | self.parse('2011-13年运输总周转量和游客周转量值分布状况?') 124 | 125 | self.parse('2011-13年运输总周转量占总体的比例的变化形势?') 126 | self.parse('2011-13年运输总周转量占父级指标比的情况?') 127 | self.parse('2011-13年运输总周转量值占总比的分布状况?') 128 | self.parse('2011-13年运输总周转量和游客周转量值占总比的分布状况?') 129 | 130 | def test_areas_trend(self): 131 | self.parse('2011-13年国内运输总周转量的变化趋势如何?') 132 | self.parse('2011-13年国际运输总周转量情况?') 133 | self.parse('2011-13年港澳台运输总周转量值分布状况?') 134 | self.parse('2011-13年港澳台和国际运输总周转量值分布状况?') 135 | 136 | self.parse('2011-13年国内运输总周转量占总体的比例的变化形势?') 137 | self.parse('2011-13年国际运输总周转量占父级指标比的情况?') 138 | self.parse('2011-13年港澳台运输总周转量值占总比的分布状况?') 139 | 140 | 141 | if __name__ == '__main__': 142 | qp = QPTest() 143 | qp.test() 144 | -------------------------------------------------------------------------------- /lib/answer.py: -------------------------------------------------------------------------------- 1 | # 回答构建器 2 | from itertools import product 3 | from types import FunctionType 4 | 5 | from pyecharts.charts import Page 6 | 7 | from lib.formatter import Formatter 8 | from const import CHART_RENDER_DIR 9 | 10 | 11 | class Answer: 12 | 13 | def __init__(self): 14 | self._answers = [] 15 | self._sub_answers = [] 16 | self._charts = [] # 保存图表 17 | 18 | def add_answer(self, string: str): 19 | self._answers.append(string) 20 | 21 | def to_string(self) -> str: 22 | return ";".join(self._answers) 23 | 24 | def save_chart(self, chart): 25 | self._charts.append(chart) 26 | 27 | def combine_charts(self): 28 | # 若有大于一个以上的图表,将它们合为一个图表(Page类型) 29 | # 不直接画到一个图表上,是因为在web app中无法直接嵌入Page类型,而其他模式均可以 30 | if len(self._charts) > 1: 31 | page = Page() 32 | page.add(*self._charts) 33 | self._charts.clear() 34 | self.save_chart(page) 35 | 36 | def get_chart(self): 37 | return None if not self.have_charts() else self._charts[0] 38 | 39 | def get_charts(self): 40 | return self._charts 41 | 42 | def render_chart(self, name: str): 43 | chart = self._charts[0] 44 | chart.render(f'{CHART_RENDER_DIR}/{name}.html') 45 | 46 | def have_charts(self): 47 | return len(self._charts) != 0 48 | 49 | def begin_sub_answers(self): 50 | self._sub_answers.clear() 51 | 52 | def add_sub_answers(self, string: str): 53 | self._sub_answers.append(string) 54 | 55 | def end_sub_answers(self): 56 | self._answers.append(','.join(self._sub_answers)) 57 | 58 | 59 | class AnswerBuilder: 60 | 61 | def __init__(self, answer: Answer): 62 | self._data = None 63 | self.answer = answer 64 | 65 | def feed_data(self, data): 66 | self._data = data 67 | 68 | @classmethod 69 | def product_name(cls, *name: list): 70 | """ 对传入的名称进行笛卡尔积 71 | eg: [1,2],[a,b] => (1,a),(1,b),(2,a),(2,b) 72 | if flatten => 1a, 1b, 2a, 2b 73 | """ 74 | for n in product(*name): 75 | if len(n) == 1: 76 | yield Formatter({'prod.name': n[0]}) 77 | else: 78 | yield Formatter({'prod.name': n[1], 'prod.area': n[0]}) 79 | 80 | @classmethod 81 | def product_repeat(cls, feeds: list, n: int): 82 | """ 对传入的列表元素每个重复n次迭代 """ 83 | count = 0 84 | i = 0 85 | for _ in range(len(feeds) * n): 86 | yield feeds[i] 87 | count += 1 88 | if count == n: 89 | count = 0 90 | i += 1 91 | 92 | @classmethod 93 | def product_binary(cls, data: list): 94 | """ 对传入的数据进行二元输出 95 | eg: [1,2,3,4] or [1,2,3,4,5] => (1,3),(2,4) 96 | """ 97 | mid = len(data) // 2 98 | start = 0 99 | end = mid 100 | while start < mid: 101 | yield data[start], data[end] 102 | start += 1 103 | end += 1 104 | 105 | @classmethod 106 | def binary_calculation(cls, operand1: str, operand2: str, operator: FunctionType, 107 | percentage: bool = False) -> float: 108 | res = None 109 | try: 110 | res = operator(float(operand1), float(operand2)) 111 | res = round(res*100, 2) if percentage else round(res, 3) 112 | finally: 113 | return res 114 | 115 | @classmethod 116 | def growth_calculation(cls, this_year: str, last_year: str) -> float: 117 | res = None 118 | try: 119 | f1, f2 = float(this_year), float(last_year) 120 | res = round(((f1 - f2) / f2) * 100, 2) 121 | finally: 122 | return res 123 | 124 | @classmethod 125 | def group_mapping_to_float(cls, x: list) -> list: 126 | """ 把x映射为float值序列 """ 127 | res = [] 128 | for e in x: 129 | try: 130 | res.append(float(e.value)) 131 | except ValueError or TypeError: 132 | res.append(0) 133 | except IndexError: 134 | res.append(0) 135 | return res 136 | 137 | def product_data_with_name(self, *names, if_is_none: FunctionType = None): 138 | for item, name in zip(self._data, self.product_name(*names)): 139 | if if_is_none is None: 140 | yield item, name 141 | else: 142 | if not item: 143 | self.answer.add_answer(if_is_none(item, name)) 144 | else: 145 | yield item, name 146 | 147 | def product_data_with_feed(self, *names, 148 | if_x_is_none: FunctionType, 149 | if_y_is_none: FunctionType): 150 | data1, data2, feed = self._data 151 | n = len(data2) // len(feed) 152 | for item1, item2, feed, name in zip(data1, data2, self.product_repeat(feed, n), 153 | self.product_name(*names)): 154 | if self.binary_decision(item1, item2, 155 | not_x=if_x_is_none(item1, item2, feed, name), 156 | not_y=if_y_is_none(item1, item2, feed, name)): 157 | yield item1, item2, feed, name 158 | 159 | def product_data_with_binary(self, *names, 160 | if_x_is_none: FunctionType, 161 | if_y_is_none: FunctionType): 162 | for item, name in zip(self.product_binary(self._data), 163 | self.product_binary([n for n in self.product_name(*names)])): 164 | x, y = item 165 | if self.binary_decision(x, y, 166 | not_x=if_x_is_none(x, y, name), 167 | not_y=if_y_is_none(x, y, name)): 168 | yield item, name 169 | 170 | # 常用逻辑模板 171 | 172 | def binary_decision(self, x, y, not_x: str, not_y: str) -> bool: 173 | if isinstance(x, list): 174 | x = any(x) 175 | if isinstance(y, list): 176 | y = any(y) 177 | dec = False 178 | if x and y: 179 | dec = True 180 | elif not x: 181 | self.answer.add_answer(not_x) 182 | else: 183 | self.answer.add_answer(not_y) 184 | return dec 185 | 186 | def add_if_is_equal_or_not(self, x, y, no: str, 187 | equal: bool = True, to_sub: bool = False) -> bool: 188 | """ equal=True时,两值相同则返回,若不同则把no加入answer; 189 | equal=False时,两值不同则返回,若相同则把no加入answer """ 190 | add_no = (x != y) if equal else (x == y) 191 | if add_no: 192 | if to_sub: 193 | self.answer.add_sub_answers(no) 194 | else: 195 | self.answer.add_answer(no) 196 | return False 197 | return True 198 | 199 | def add_if_is_not_none(self, x, no: str, to_sub: bool = True) -> bool: 200 | """ 如果x为None,就把no加入answers中 """ 201 | if x is None: 202 | if to_sub: 203 | self.answer.add_sub_answers(no) 204 | else: 205 | self.answer.add_answer(no) 206 | return False 207 | return True 208 | -------------------------------------------------------------------------------- /web/static/js/func.js: -------------------------------------------------------------------------------- 1 | // web app 中最基本的功能 2 | 3 | var inputCount = 0; 4 | let flag = true; // null placeholder 5 | 6 | let keywords = [ 7 | '运输航空', '运输周转量', '旅客运输量', '货邮运输量', '旅客吞吐量', 8 | '东部地区', '东北地区', '中部地区', '西部地区', '货邮吞吐量', '起降架次', 9 | '大中型飞机', '小型飞机', '经济效益', '主要航空公司', '安康杯', 10 | '固定资产投资', '一二三三四', '通用航空企业', '消费者投诉', '驾驶员执照', '2017', 11 | '飞行员数量', '航空货物', '宽体飞机', '窄体飞机', '支线飞机', '服务满意度', '空管系统四类专业技术人员', '严重失信人名单', 12 | '一加快、两实现' 13 | ]; 14 | let keynotes = [ 15 | '各项数据为正式年报数据,部分统计数据与此前公布的初步统计数据如有出入,以本次公布数据为准。', 16 | '涉及的数据为国内航空公司承运的数据。', 17 | '涉及的数据为国内航空公司承运的数据。', 18 | '涉及的数据为国内航空公司承运的数据。', 19 | '指报告期内进港(机场)和出港的旅客人数。', 20 | '指北京、上海、山东、江苏、天津、浙江、海南、河北、福建和广东10省市。', 21 | '指黑龙江、辽宁和吉林3省。', 22 | '指江西、湖北、湖南、河南、安徽和山西6省。', 23 | '指宁夏、陕西、云南、内蒙古、广西、甘肃、贵州、西藏、新疆、重庆、青海和四川12省(区、市)。', 24 | '指报告期内货物和邮件的进出港数量。', 25 | '指报告期内在机场进出港飞机的全部起飞和降落次数,起飞、降落各算一架次。', 26 | '指座级在100座以上的运输飞机。', 27 | '指座级在100座以下的运输飞机。', 28 | '涉及数据为财务快报数据,最终数据以财务年报数据为准。', 29 | '指南航、国航、东航、海南、深圳、四川、厦门、山东、上海、天津等十家航空公司。', 30 | '安全生产荣誉奖杯。“安康杯”竞赛活动旨在通过竞赛不断推进企事业单位安全生产工作和安全文化
建设,提高全民安全生产意识,从而降低各类事故的发生率和各类职业病的发病率。', 31 | '未含飞机和特种车辆购租等投资。', 32 | '指的是“践行一个理念、 推动两翼齐飞、 坚守三条底线、 完善三张网络、 补齐四个短板” 的总体工作思路。', 33 | '通用航空企业地区分布按民航各地区管理局所辖区域划分。', 34 | '投诉总数包含旅客与企业自行解决的首次电话投诉。', 35 | '存在一个飞行员取得多个执照的情况。', 36 | '2017年度行业综合统计系统正式上线后,由于航空公司生产数据处理流程
和审核规则发生变化,2017年度相关数据和增速按照最新的口径进行计算。', 37 | '飞行员数量来自民航局飞标司', 38 | '不含邮件、快件', 39 | '250座级以上的运输飞机', 40 | '100-200座级运输飞机', 41 | '100座级以下运输飞机', 42 | '满分5分,根据中国民航科学技术研究院、中国民航报社、航旅纵横、
中国民航机场协会共同发布的《年度中国民航服务旅客满意度评价报告》。', 43 | '包括空中交通管制员、航空电信人员、航空情报人员和航空气象人员。', 44 | '依据《关于在一定期限内适当限制特定严重失信人乘坐民用航空器推动社会信用体系建设的意见》
(发改财金〔 2018〕 385 号)相关规定。', 45 | '根据《新时代民航强国建设行动纲要》提出的战略步骤,到2020年,
民航加快实现从航空运输大国向航空运输强国的跨越;从2021年到2035年,
实现从单一的航空运输强国向多领域的民航强国的跨越;到本世纪中叶,
实现由多领域的民航强国向全方位的民航强国的跨越。' 46 | ]; 47 | 48 | $(function () { 49 | // 输入框:回车键发送 50 | let inputArea = $('#input-dialog'); 51 | inputArea.keydown(function (e) { 52 | if(e.keyCode === 13) { 53 | e.preventDefault(); 54 | send_question(); 55 | } 56 | }) 57 | 58 | // 按钮区:清空按钮 59 | $('#clear-btn').click(function () { 60 | inputArea.val(""); 61 | }); 62 | // 发送按钮 63 | $('#send-btn').click(send_question); 64 | 65 | // 输出框 66 | let outputArea = document.querySelector('#output-area'); 67 | let nullHeight = outputArea.clientHeight; 68 | let nullArea = document.querySelector('.null'); 69 | 70 | function send_question() { 71 | // get question string 72 | let question = inputArea.val(); 73 | if(question === '') { 74 | alert('问题输入不可为空'); 75 | return ; 76 | } 77 | // increase input count 78 | inputCount++; 79 | // create question element 80 | let questionElm = document.createElement('div'); 81 | questionElm.className = 'question'; 82 | questionElm.innerText = '[Q' + inputCount + ']:' + question; 83 | // clear input area 84 | outputArea.appendChild(questionElm); 85 | inputArea.val(""); 86 | // add to output area 87 | stick_to_bottom(); 88 | // send it to server 89 | $.ajax({ 90 | url: '/send', 91 | type: 'GET', 92 | dataType: 'json', 93 | contentType: 'application/json', 94 | data: { 95 | 'question': question, 96 | }, 97 | success: function (data) { 98 | // console.log(data); 99 | setTimeout(function () { 100 | add_answer(data); 101 | }, 500) 102 | }, 103 | error: function (msg) { 104 | // console.log(msg); 105 | show_error(msg); 106 | } 107 | }); 108 | } 109 | 110 | function add_answer(data) { 111 | // create answer element 112 | let answerElm = document.createElement('div'); 113 | answerElm.className = 'answer'; 114 | // link note to it 115 | let answer = link_note(data['answer']); 116 | answerElm.innerHTML = '[A' + inputCount + ']:' + answer; 117 | // add to output area 118 | outputArea.appendChild(answerElm); 119 | init_note(); 120 | // if it has chart 121 | for(var i = 0; i < data['chart_count']; i++) { 122 | // create chart canvas 123 | let chartElm = document.createElement('div'); 124 | chartElm.className = 'answer-chart'; 125 | chartElm.id = 'chart-' + inputCount + '-' + i; 126 | // add to output area 127 | outputArea.appendChild(chartElm); 128 | get_chart(chartElm.id, i); 129 | } 130 | stick_to_bottom(); 131 | } 132 | 133 | function get_chart(chart_id, chart_index) { 134 | var chart = echarts.init(document.getElementById(chart_id), 'white', {renderer: 'canvas'}); 135 | $.ajax({ 136 | type: 'GET', 137 | url: '/chart', 138 | dataType: 'json', 139 | contentType: 'application/json', 140 | data: { 141 | 'chart_index': chart_index, 142 | }, 143 | success: function (result) { 144 | chart.setOption(result); 145 | }, 146 | error: function (msg) { 147 | // console.log(msg); 148 | show_error(msg); 149 | } 150 | }); 151 | } 152 | 153 | // 给出关键词语的注释 154 | function link_note(answer) { 155 | for(var i=0; i'+answer.slice(j,end)+''+answer.slice(end,answer.length); 160 | } 161 | } 162 | return answer; 163 | } 164 | 165 | function init_note() { 166 | $('[data-tooltip]').addClass('tooltip'); 167 | $('.tooltip').each(function () { 168 | $(this).append('' + $(this).attr('data-tooltip') + ''); 169 | }) 170 | $('.tooltip').mouseover(function () { 171 | $(this).children('.tooltip-content').css('visibility', 'visible'); 172 | }).mouseout(function () { 173 | $(this).children('.tooltip-content').css('visibility', 'hidden'); 174 | }); 175 | } 176 | 177 | // 错误 178 | function show_error(msg) { 179 | let errElm = document.createElement('div'); 180 | errElm.className = 'answer-error'; 181 | errElm.innerText = msg.toString(); 182 | outputArea.appendChild(errElm); 183 | stick_to_bottom(); 184 | } 185 | 186 | // 保持发出的问题框在最底下 187 | function stick_to_bottom() { 188 | // always stick to bottom 189 | let n = outputArea.scrollHeight - outputArea.clientHeight; 190 | outputArea.scrollTop = n; 191 | if(flag) { 192 | nullHeight -= n; 193 | nullArea.style.height = nullHeight + 'px'; 194 | if(nullHeight < 0) { 195 | nullArea.style.height = '0'; 196 | flag = false; 197 | } 198 | } 199 | } 200 | 201 | }); 202 | -------------------------------------------------------------------------------- /web/templates/macro: -------------------------------------------------------------------------------- 1 | {%- macro render_chart_content(c) -%} 2 |
3 | 25 | {%- endmacro %} 26 | 27 | {%- macro render_notebook_charts(charts, libraries) -%} 28 | 47 | {%- endmacro %} 48 | 49 | {%- macro render_chart_dependencies(c) -%} 50 | {% for dep in c.dependencies %} 51 | 52 | {% endfor %} 53 | {%- endmacro %} 54 | 55 | {%- macro render_chart_css(c) -%} 56 | {% for dep in c.css_libs %} 57 | 58 | {% endfor %} 59 | {%- endmacro %} 60 | 61 | {%- macro display_tablinks(chart) -%} 62 |
63 | {% for c in chart %} 64 | 65 | {% endfor %} 66 |
67 | {%- endmacro %} 68 | 69 | {%- macro switch_tabs() -%} 70 | 93 | {%- endmacro %} 94 | 95 | {%- macro generate_tab_css() %} 96 | 127 | {%- endmacro %} 128 | 129 | {%- macro gen_components_content(chart) %} 130 | {% if chart._component_type == "table" %} 131 | 183 |
184 |

{{ chart.title_opts.title }}

185 |

{{ chart.title_opts.subtitle }}

186 | {{ chart.html_content }} 187 |
188 | {% elif chart._component_type == "image" %} 189 |
190 |

{{ chart.title_opts.title }}

191 |

{{ chart.title_opts.subtitle }}

192 | 193 |
194 | {% endif %} 195 | {%- endmacro %} 196 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CivilAviation Q&A 2 | 3 | 基于民航业知识图谱的自动问答系统。 4 | 5 | A QA system based on Civil Aviation knowledge graph. 6 | 7 | ## 平台 8 | - Windows 10 x64 9 | - Python 3.7 10 | - Neo4j community 3.5.20 11 | 12 | ## 运行 13 | 1. 确保安装所需依赖 14 | ``` 15 | pip install -r requirements.txt 16 | ``` 17 | 注:`python-Levenshtein` 如果安装不成功,则可以下载对其进行离线安装。 18 | 19 | 2. 构建知识图谱 20 | 21 | 修改`const.py`文件中连接数据库使用的`URI`,`USERNAME`和`PASSWORD`的值。然后执行: 22 | ``` 23 | python build_cakg.py 24 | ``` 25 | 运行大约需要2~5分钟。 26 | 27 | 3. 可以使用两种方式运行: 28 | 1. 运行命令行端 29 | ``` 30 | python run_cmd.py 31 | ``` 32 | 普通问题的回答以字符串的形式给出;带有图表的回答,图表会被渲染至`results`文件夹中。 33 | 2. 运行web端(效果图见下文) 34 | ``` 35 | python run_web.py 36 | ``` 37 | 带有图表的回答和普通回答一样会被渲染至web页面中,同时也被保存至本地`results`文件夹中。 38 | 39 | 注1:最好使用谷歌浏览器(Google Chrome); 40 | 41 | 注2:生成图表的文件夹地址可以在`const.py`中更改`CHART_RENDER_DIR`。 42 | 4. have fun! 43 | 44 | ## 简介 45 | ### 一. 项目结构 46 | ``` 47 | --------------------------------------- root 48 | |------data/ # 数据存放 49 | |------dicts/ # 存放特征词(运行build_cakg.py后自动生成) 50 | |------question/ # 存放问句中的疑问词 51 | |------reference/ # 存放指代词 52 | |------tail/ # 存放尾词(后缀词) 53 | |------data.json # 从年报中组织出的数据 54 | |------raw.7z # 11-19年的年报 55 | |------demo/ # 以jupyter-notebook的形式给出了各种问题类型的演示和说明 56 | |------doc/ # 存放有关readme的文件 57 | |------lib/ # 函数库 58 | |------results/ # 存放某些问题生成的图表(会自动生成) 59 | |------test/ # 存放一些单元测试 60 | |------web/ # web app 61 | ...... 62 | |------answer_search.py # 回答组织器 63 | |------build_cakg.py # 构建知识图谱 64 | |------chatbot.py # 自动问答器 65 | |------const.py # 常量 66 | |------question_classifier.py # 分类器 67 | |------question_parser.py # 解析器 68 | ...... 69 | ``` 70 | 71 | ### 二. 数据组织 72 | #### 1. 基本构想 73 | 通过浏览公报发现: 74 | 1. 每一年所涉及的目录大差不差,有时多有时少,或者只是改个名字; 75 | 2. 目录中涉及的指标每年都有一定的变动,而且某些指标里面嵌套指标,还有些指标中给出了各地区的组成值; 76 | 3. 指标的值有数值类型,也有字符串类型,有的有单位,有些则没有,而且有些单位在某些年份还不同。 77 | 78 | 基于上述几点,我将知识图谱的构建以年份为中心展开,将各个目录、指标等等实体作为知识图谱的结点。结点与结点之间相连接的关系称为`结构关系`(详细见下文),那么将每个年份结点到各个指标和地区的关系称为`值关系`(详细见下文)。将结构和值两种关系拆开, 79 | 80 | 1. 从结构关系来看,不用一个年度录入一个年度的所有指标,每个年度中肯定有重复指标,这样避免了数据冗余。若每年的指标位置基本不变,则上述做法直接可行,但实际上指标出现的位置可能每年都飘忽不定,所以若直接按上述做法会出现这种情况: 81 | 82 | `假设2012年指标C1包含指标A、B,指标C2包含指标C;2013年指标C1包含指标A,指标C2包含指标B、C;则其结构关系为:` 83 | 84 | ![rel](doc/struct.png) 85 | 86 | 其中橙色的边是2012年特有的,蓝色的则是2013年特有的,而黑色的是它们共有的。但在知识图谱中这些边没有颜色之分,是按上图整个结构存储的,这就造成了一个父子结构关系错乱的问题,比如:我要查找13年指标C1包含的所有指标,则A和B都会被返回,而实际上B不应该被返回。 87 | 88 | 为了解决上述问题,并且不增加任何额外的关系,我为每个关系引入了一个生命周期属性`life`。这个属性运用了掩码的思想,每个年份维护自己的掩码(运行构建知识图谱脚本时会被自动生成),在遇到上述问题时,拿来和关系中的life做`与`运算,若结果不为0,就说明此年份包含此指标,反之则不含。 89 | 2. 从值关系来看,问题中也是直接给出年份和指标名称,这样也方便查询。 90 | 91 | 部分结点间的关系如下图:(橙色为年份,棕色为目录,蓝色为指标) 92 | 93 | ![kg1](doc/graph-1.png) 94 | 95 | 部分结点间的关系如下图:**(橙色为年份,棕色为目录,蓝色为指标,红色为地区/机场/公司集团,下同)** 96 | 97 | ![kg2](doc/graph-2.png) 98 | 99 | #### 2. 知识图谱实体类型 100 | | 实体类型 | 含义 | 数量 | 举例 | 101 | |--|--|--|--| 102 | | Year | 年份 | 9 | 2011,2012 | 103 | | Catalog | 目录 | 16 | 运输航空,航空安全 | 104 | | Index | 指标 | 242 | 运输总周转量,货邮运输量 | 105 | | Area | 地区/机场/公司集团 | 35 | 港澳台,中航集团 | 106 | 107 | #### 3. 知识图谱实体关系类型 108 | | 实体结构关系类型 | 含义 | 数量 | 举例 | 109 | |--|--|--|--| 110 | | include | 年度包括目录、目录包括指标 | 193 | ![include](doc/graph-include.png)| 111 | | contain | 指标(父)含有指标(子)、地区(父)含有地区(子) | 148 | ![contain](doc/graph-contain.png)| 112 | | locate | 指标位于某地区 | 93 | ![locate](doc/graph-locate.png)| 113 | 114 | | 实体值关系类型 | 含义 | 数量 | 举例 | 115 | |--|--|--|--| 116 | | info | 某年度的目录信息 | 45 | ![info](doc/graph-info.png)| 117 | | value | 某年度的指标值 | 1194 | ![value](doc/graph-value.png) | 118 | | 具体的指标名称 | 某年度某地区的指标值 | 781 | ![index-name](doc/graph-indexname.png)| 119 | 120 | #### 4. 知识图谱属性类型 121 | | 属性类型 | 含义 | 举例 | 122 | |--|--|--| 123 | | info | 年份或目录的整体概况 | 全年航空安全形势稳定,旅客运输和... | 124 | | name | 各实体名称 | 2014,国内,货邮周转量 | 125 | | value | 指标或地区的值 | 451.2,0,22 | 126 | | unit | 指标或地区值的单位 | 亿吨公里,%,万元 | 127 | | repr | 地区值所代表的含义 | 航线(不含港澳台),吞吐量占比 | 128 | | child_id | 同一(父)指标下不同(子)指标拥有相同child_id的可视为构成父指标的一个角度 | 0,1,2 ... | 129 | | life | (结构关系)生命周期 | 0x179, 0x008 | 130 | 131 | ### 三. 问题预处理 132 | 主要指年份和指标两个角度的预处理,此部分详见`lib/complement.py`。 133 | 1. 年份角度 134 | 对问题中的年份进行替换,方便特征词识别,例: 135 | ``` 136 | 11年 -> 2011年 137 | 两千一十一年 -> 2011年 138 | 11-15年 -> 2011年,2012年,2013年,2014年,2015年 139 | 13到15年 -> 2013年,2014年,2015年 140 | 141 | 13年比前年 -> 2013年比2011年 142 | 15年比大大前年 -> 2015年比2011年 143 | 144 | 16年比3年前 -> 2016年比2013年 145 | 16年与前三年相比 -> 2016年与2015年,2014年,2013年相比 146 | ...... 147 | ``` 148 | 2. 指标角度 149 | 对问题中的指标名进行替换,避免因错字漏字而特征词识别不成功。通过`Levenshetin`算法实现对指标名的模糊查询。例: 150 | ``` 151 | 游客周转量 -> 旅客周转量 152 | ...... 153 | ``` 154 | 155 | ### 四. 问题分类 156 | 问题的分类是基于特征词的分类,使用`ahocorasick`算法。 157 | 158 | 下表给出的是各种问题的类型,更详细的内容请参见项目`demo`中的`demo1~4.ipynb`。 159 | 160 | | 问题类型 | 含义 | 举例 | 161 | |--|--|--| 162 | | year_status | 年度整体状况 | 2011年整体状况如何? | 163 | | catalog_status | 年度目录状况 | 2012年教育及科技状况如何? | 164 | | catalog_change | 年度间目录变化 | 12年比11年少了哪些目录? | 165 | | index_change | 年度间指标变化 | 13年比前年增加了哪些指标 ?| 166 | | exist_catalog | 年度有哪些目录 | 11年有哪些目录? | 167 | | begin_stats | 指标何时开始统计 | 在哪年最先出现了航空公司营业收入数据? | 168 | | index_value | 指标值 | 13年货邮周转量为? | 169 | | index_overall | 指标占总比 | 13年旅客周转量占其总体的百分之几? | 170 | | index_2_overall | 指标占总比的变化 | 13年运输总周转量占其父级指标的倍数比11年降低了多少? | 171 | | indexes_m_compare | 指标的倍数比较 | 11年旅客周转量是新增机场数量的几倍? | 172 | | indexes_n_compare | 指标的和差比较 | 12年旅客周转量比货邮周转量少多少? | 173 | | indexes_g_compare | 指标的同比比较 | 11年旅客周转量同比增长 | 174 | | indexes_2m_compare | 年度之间指标的倍数比较 | 12年新增机场数量是11年的几倍? | 175 | | indexes_2n_compare | 年度之间指标的和差比较 | 12年比13年货邮运输量增加了多少? | 176 | | area_value | 地区指标值 | 11年国内运输总周转量是? | 177 | | area_overall | 地区指标占总比 | 11年港澳台运输总周转量占其父级地区指标的百分之几? | 178 | | area_2_overall | 地区指标占总比的变化 | 12年港澳台的旅客运输量占总体的百分比比去年变化了多少? | 179 | | areas_m_compare | 地区指标的倍数比较 | 13年国内货邮周转量是国际的几倍? | 180 | | areas_n_compare | 地区指标的和差比较 | 11年港澳台旅客运输量比国内的少多少? | 181 | | areas_g_compare | 地区指标的同比比较 | 12年国内旅客运输量同比变化? | 182 | | areas_2m_compare | 年度之间地区指标的倍数比较 | 13年国内货邮周转量占11年的百分之几? | 183 | | areas_2n_compare | 年度之间地区指标的和差比较 | 13年国内运输总周转量比12年多多少? | 184 | | index_compose | 指标的组成 | 13年运输总周转量的子集有? | 185 | | indexes_trend | 指标的变化 | 11-14年节能减排的情况? | 186 | | areas_trend | 地区指标的变化 | 11-13年港澳台运输总周转量分布状况? | 187 | | indexes_overall_trend | 指标占总比的变化 | 11-13年货邮运输量占总体指标的比例的变化? | 188 | | areas_overall_trend | 地区指标占总比的变化 | 11-13年港澳台运输总周转量占其总体地区的比值变化情况? | 189 | | indexes_change | 指标个数的变化 | 11-13年指标变化情况? | 190 | | catalogs_change | 目录个数的变化 | 11-13年目录变化情况? | 191 | | indexes_max | 指标的最值 | 11-13年货邮周转量的最大值? | 192 | 193 | ### 五. Web APP 194 | web端使用Flask构建,采用前后端分离的方式。问答界面较为简洁。但可实现以下功能: 195 | 1. 回答带有的图表可以直接渲染至页面; 196 | 2. 回答中某些关键词以`tooltips`的形式进行了解释说明(关键词取自年报的注释部分)。 197 | 198 | ![web1](doc/web-1.png) 199 | 200 | ![web2](doc/web-2.png) 201 | 202 | ![web3](doc/web-3.png) 203 | 204 | ## 说明 205 | 1. **项目因经过多次重构,故难免有些晦涩之处,欢迎提问**; 206 | 2. **数据组织或问题分类难免有不足之处,若有更好的想法,欢迎提出**; 207 | 3. **单元测试对各种问题只能尽可能多的照顾到,若有什么问题,欢迎指正**; 208 | 4. 若有疑问,留言、私信、open issue都可,我将尽可能及时回复; 209 | 5. 若有BUG,请将 “所问问题” ,“报错” 或其他有用信息通过上述方式提供。 210 | 211 | ## 参考 212 | 1. 项目结构参考:https://github.com/liuhuanyong/QASystemOnMedicalKG 213 | 2. web app 中 tooltips 的实现参考:http://www.webkaka.com/tutorial/html/2020/080295/ 214 | 3. 数据均取自中国民用航空局发布的2011~2019年《年度民航行业发展统计公布》 215 | -------------------------------------------------------------------------------- /question_parser.py: -------------------------------------------------------------------------------- 1 | # 问题解析器 2 | from copy import deepcopy 3 | 4 | from lib.result import Result 5 | from lib.chain import TranslationChain 6 | from lib.errors import QuestionYearOverstep 7 | 8 | 9 | class QuestionParser: 10 | 11 | def __init__(self): 12 | self.chain = TranslationChain() 13 | 14 | # 基本sql语句, 供翻译方法使用 15 | self.sql_Y_status = 'match (y:Year) where y.name="{y}" return y.info' 16 | self.sql_C_status = 'match (y:Year)-[r:info]->(c:Catalog) where y.name="{y}" and c.name="{c}" return r.info' 17 | self.sql_I_value = 'match (y:Year)-[r:value]->(i:Index) where y.name="{y}" and i.name="{i}" return r.value,r.unit' 18 | self.sql_A_value = 'match (y:Year)-[r:`{i}`]->(n:Area) where y.name="{y}" and n.name="{a}" return r.value,r.unit,r.repr' 19 | 20 | self.sql_find_I_parent = 'match (n:Index)-[r:contain]->(m:Index) where m.name="{i}" return n.name,r.life' 21 | self.sql_find_A_parent = 'match (n:Area)-[r:contain]->(m:Area) where m.name="{a}" return n.name,r.life' 22 | self.sql_find_I_child = 'match (n:Index)-[r]->(m) where n.name="{i}" return m.name,labels(m)[0],r.life' 23 | self.sql_find_Is = 'match (y:Year)-[r:value]->(i:Index) where y.name="{y}" return i.name' 24 | self.sql_find_Cs = 'match (y:Year)-[r:include]->(c:Catalog) where y.name="{y}" return c.name' 25 | self.sql_find_begin_stats_Ys = 'match (y:Year)-[r:value]->(i:Index) where i.name="{i}" return y.name' 26 | 27 | def parse(self, result: Result) -> Result: 28 | for qt in result.question_types: 29 | # 查询语句翻译 30 | if qt == 'year_status': 31 | self.trans_year_status(result['year']) 32 | elif qt == 'catalog_status': 33 | self.trans_catalog_status(result['year'], result['catalog']) 34 | elif qt == 'exist_catalog': 35 | self.trans_exist_catalog(result['year']) 36 | elif qt in ('index_value', 'indexes_m_compare', 'indexes_n_compare'): 37 | self.trans_index_value(result['year'], result['index']) 38 | elif qt == 'index_overall': 39 | self.trans_index_overall(result['year'], result['index']) 40 | elif qt in ('index_2_overall', 'indexes_overall_trend'): 41 | self.trans_indexes_overall(result['year'], result['index']) 42 | elif qt == 'index_compose': 43 | self.trans_index_compose(result['year'], result['index']) 44 | elif qt in ('indexes_2m_compare', 'indexes_2n_compare'): 45 | self.trans_indexes_mn_compare(result['year'], result['index']) 46 | elif qt == 'indexes_g_compare': 47 | self.trans_indexes_g_compare(result['year'], result['index']) 48 | elif qt in ('area_value', 'areas_m_compare', 'areas_n_compare'): 49 | self.trans_area_value(result['year'], result['area'], result['index']) 50 | elif qt == 'area_overall': 51 | self.trans_area_overall(result['year'], result['area'], result['index']) 52 | elif qt in ('area_2_overall', 'areas_overall_trend'): 53 | self.trans_areas_overall(result['year'], result['area'], result['index']) 54 | elif qt in ('areas_2m_compare', 'areas_2n_compare'): 55 | self.trans_areas_mn_compare(result['year'], result['area'], result['index']) 56 | elif qt == 'areas_g_compare': 57 | self.trans_areas_g_compare(result['year'], result['area'], result['index']) 58 | elif qt in ('indexes_trend', 'indexes_max'): 59 | self.trans_indexes_value(result['year'], result['index']) 60 | elif qt in ('areas_trend', 'areas_max'): 61 | self.trans_areas_value(result['year'], result['area'], result['index']) 62 | elif qt in ('index_change', 'indexes_change'): 63 | self.trans_index_change(result['year']) 64 | elif qt in ('catalog_change', 'catalogs_change'): 65 | self.trans_catalog_change(result['year']) 66 | elif qt == 'begin_stats': 67 | self.trans_begin_stats(result['index']) 68 | 69 | result.add_sql(qt, deepcopy(self.chain)) 70 | self.chain.reset() 71 | return result 72 | 73 | # 年度总体状况 74 | def trans_year_status(self, years): 75 | self.chain.make([self.sql_Y_status.format(y=years[0])]) 76 | 77 | # 年度目录状况 78 | def trans_catalog_status(self, years, catalogs): 79 | self.chain.make([self.sql_C_status.format(y=years[0], c=c) for c in catalogs]) 80 | 81 | # 指标变化情况 82 | def trans_index_change(self, years): 83 | self.chain.make([self.sql_find_Is.format(y=y) for y in years]) 84 | 85 | # 目录变化情况 86 | def trans_catalog_change(self, years): 87 | self.chain.make([self.sql_find_Cs.format(y=y) for y in years]) 88 | 89 | # 年度目录包含哪些 90 | def trans_exist_catalog(self, years): 91 | self.chain.make([self.sql_find_Cs.format(y=years[0])]) 92 | 93 | # 指标值 94 | def trans_index_value(self, years, indexes): 95 | self.chain.make([self.sql_I_value.format(y=years[0], i=i) for i in indexes]) 96 | 97 | # 多个年份指标值变化趋势 98 | def trans_indexes_value(self, years, indexes): 99 | self.chain.make([[self.sql_I_value.format(y=y, i=i) for y in years] for i in indexes]) 100 | 101 | # 两个年份下的指标值的各种比较 102 | def trans_indexes_mn_compare(self, years, indexes): 103 | self.chain.make([[self.sql_I_value.format(y=y, i=i) for y in years] for i in indexes]) 104 | 105 | # 指标值同比比较 106 | def trans_indexes_g_compare(self, years, indexes): 107 | last_year = int(years[0])-1 108 | QuestionYearOverstep.check(last_year) 109 | self.chain.make([[self.sql_I_value.format(y=last_year, i=i), 110 | self.sql_I_value.format(y=years[0], i=i)] for i in indexes]) 111 | 112 | # 指标占总比 113 | def trans_index_overall(self, years, indexes): 114 | self.chain.make([self.sql_I_value.format(y=years[0], i=i) for i in indexes])\ 115 | .then([self.sql_find_I_parent.format(i=i) for i in indexes])\ 116 | .then([self.sql_I_value.format(y=years[0], i='{}')]) 117 | 118 | # 两或多个年份指标占总比的变化 119 | def trans_indexes_overall(self, years, indexes): 120 | self.chain.make([[self.sql_I_value.format(y=y, i=i) for y in years] for i in indexes])\ 121 | .then([self.sql_find_I_parent.format(i=i) for i in indexes])\ 122 | .then([self.sql_I_value.format(y=y, i='{}') for y in years]) 123 | 124 | # 指标组成 125 | def trans_index_compose(self, years, indexes): 126 | self.chain.make([self.sql_find_I_child.format(i=i) for i in indexes]) \ 127 | .then([self.sql_I_value.format(y=years[0], i='{}') + ',r.child_id']) \ 128 | .then([self.sql_A_value.format(y=years[0], i='{}', a='{}') + ',r.child_id']) \ 129 | .then([self.sql_I_value.format(y=years[0], i=i) for i in indexes]) # overall 130 | 131 | # 地区指标值 132 | def trans_area_value(self, years, areas, indexes): 133 | self.chain.make([self.sql_A_value.format(y=years[0], i=i, a=a) for a in areas for i in indexes]) 134 | 135 | def trans_areas_value(self, years, areas, indexes): 136 | self.chain.make([[[self.sql_A_value.format(y=y, i=i, a=a) for y in years] for a in areas] for i in indexes]) 137 | 138 | # 两个年份下地区的指标值的各种比较 139 | def trans_areas_mn_compare(self, years, areas, indexes): 140 | self.chain.make([[[self.sql_A_value.format(y=y, a=a, i=i) for y in years] for a in areas] for i in indexes]) 141 | 142 | # 地区指标值同比比较 143 | def trans_areas_g_compare(self, years, areas, indexes): 144 | last_year = int(years[0]) - 1 145 | QuestionYearOverstep.check(last_year) 146 | self.chain.make([[self.sql_A_value.format(y=last_year, a=areas[0], i=i), 147 | self.sql_A_value.format(y=years[0], a=areas[0], i=i)] for i in indexes]) 148 | 149 | # 地区指标占总比 150 | def trans_area_overall(self, years, areas, indexes): 151 | self.chain.make([self.sql_A_value.format(y=years[0], i=i, a=a) for a in areas for i in indexes])\ 152 | .then([self.sql_find_A_parent.format(a=a) for a in areas])\ 153 | .then([self.sql_A_value.format(y=years[0], i=i, a='{}') for i in indexes]) 154 | 155 | # 地区两或多个年份指标占总比的变化 156 | def trans_areas_overall(self, years, areas, indexes): 157 | self.chain.make([[[self.sql_A_value.format(y=y, i=i, a=a) for y in years] for a in areas] for i in indexes])\ 158 | .then([self.sql_find_A_parent.format(a=a) for a in areas])\ 159 | .then([[self.sql_A_value.format(y=y, i=i, a='{}') for y in years] for i in indexes]) 160 | 161 | # 何时开始统计此项指标 162 | def trans_begin_stats(self, indexes): 163 | self.chain.make([self.sql_find_begin_stats_Ys.format(i=i) for i in indexes]) 164 | -------------------------------------------------------------------------------- /demo/demo1.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "metadata": { 7 | "collapsed": true 8 | }, 9 | "outputs": [ 10 | { 11 | "name": "stdout", 12 | "output_type": "stream", 13 | "text": [ 14 | "欢迎与小航对话,请问有什么可以帮助您的?\n" 15 | ] 16 | } 17 | ], 18 | "source": [ 19 | "import os\n", 20 | "from chatbot import CAChatBot\n", 21 | "\n", 22 | "os.chdir(os.path.join(os.getcwd(), '..'))\n", 23 | "chatbot = CAChatBot()" 24 | ] 25 | }, 26 | { 27 | "cell_type": "markdown", 28 | "source": [ 29 | "### 在这个demo中,你将看到如下几个问题类型的回答和介绍:\n", 30 | "1. year_status\n", 31 | "2. catalog_status\n", 32 | "3. catalog_change\n", 33 | "4. index_change\n", 34 | "5. exist_catalog\n", 35 | "6. begin_stats" 36 | ], 37 | "metadata": { 38 | "collapsed": false 39 | } 40 | }, 41 | { 42 | "cell_type": "markdown", 43 | "source": [ 44 | "## 1. year_status\n", 45 | "回答一个年度的整体情况。" 46 | ], 47 | "metadata": { 48 | "collapsed": false 49 | } 50 | }, 51 | { 52 | "cell_type": "code", 53 | "execution_count": 2, 54 | "outputs": [ 55 | { 56 | "data": { 57 | "text/plain": "'2011年,全年航空安全形势稳定,旅客运输和通用航空保持较快增长,运行质量和经济效益得到提升,基础设施建设取得新成绩,结构调整和深化改革迈出新步伐,党的建设和行业文化建设得到加强。'" 58 | }, 59 | "execution_count": 2, 60 | "metadata": {}, 61 | "output_type": "execute_result" 62 | } 63 | ], 64 | "source": [ 65 | "chatbot.query('2011年的整体状况如何?')" 66 | ], 67 | "metadata": { 68 | "collapsed": false, 69 | "pycharm": { 70 | "name": "#%%\n" 71 | } 72 | } 73 | }, 74 | { 75 | "cell_type": "markdown", 76 | "source": [ 77 | "## 2. catalog_status\n", 78 | "回答某个年度某项目录的整体情况,有些目录有这些整体情况的概述,有些没有。" 79 | ], 80 | "metadata": { 81 | "collapsed": false 82 | } 83 | }, 84 | { 85 | "cell_type": "code", 86 | "execution_count": 4, 87 | "outputs": [ 88 | { 89 | "data": { 90 | "text/plain": "'航空安全在2013年,民航安全形势平稳。全行业未发生运输航空事故、空防安全事故,发生通用航空事故10起。'" 91 | }, 92 | "execution_count": 4, 93 | "metadata": {}, 94 | "output_type": "execute_result" 95 | } 96 | ], 97 | "source": [ 98 | "chatbot.query('2013年航空安全发展形势怎么样?')" 99 | ], 100 | "metadata": { 101 | "collapsed": false, 102 | "pycharm": { 103 | "name": "#%%\n" 104 | } 105 | } 106 | }, 107 | { 108 | "cell_type": "code", 109 | "execution_count": 5, 110 | "outputs": [ 111 | { 112 | "data": { 113 | "text/plain": "'并没有关于2012年教育及科技的描述。'" 114 | }, 115 | "execution_count": 5, 116 | "metadata": {}, 117 | "output_type": "execute_result" 118 | } 119 | ], 120 | "source": [ 121 | "chatbot.query('2012年教育及科技状况如何?')" 122 | ], 123 | "metadata": { 124 | "collapsed": false, 125 | "pycharm": { 126 | "name": "#%%\n" 127 | } 128 | } 129 | }, 130 | { 131 | "cell_type": "markdown", 132 | "source": [ 133 | "## 3. catalog_change\n", 134 | "回答某两个年度相比,它们俩之间相差哪些目录;若某一年度的目录正好涵盖了另一年度的,则回答两年度目录相同。" 135 | ], 136 | "metadata": { 137 | "collapsed": false 138 | } 139 | }, 140 | { 141 | "cell_type": "code", 142 | "execution_count": 6, 143 | "outputs": [ 144 | { 145 | "data": { 146 | "text/plain": "'2011年与2013年相比,未统计3个目录:规章发布、工会工作、飞行员数量;2013年与2011年的目录相同。'" 147 | }, 148 | "execution_count": 6, 149 | "metadata": {}, 150 | "output_type": "execute_result" 151 | } 152 | ], 153 | "source": [ 154 | "chatbot.query('13年比11年多了哪些目录?')" 155 | ], 156 | "metadata": { 157 | "collapsed": false, 158 | "pycharm": { 159 | "name": "#%%\n" 160 | } 161 | } 162 | }, 163 | { 164 | "cell_type": "markdown", 165 | "source": [ 166 | "上述回答注意主语,前半句说的是2011年的目录(比2013年的少),后半句是2013年的目录涵盖了2011年的。" 167 | ], 168 | "metadata": { 169 | "collapsed": false 170 | } 171 | }, 172 | { 173 | "cell_type": "code", 174 | "execution_count": 7, 175 | "outputs": [ 176 | { 177 | "data": { 178 | "text/plain": "'2011年与2012年相比,未统计1个目录:飞行员数量;2012年与2011年的目录相同。'" 179 | }, 180 | "execution_count": 7, 181 | "metadata": {}, 182 | "output_type": "execute_result" 183 | } 184 | ], 185 | "source": [ 186 | "chatbot.query('12比11年少了哪些目录?')" 187 | ], 188 | "metadata": { 189 | "collapsed": false, 190 | "pycharm": { 191 | "name": "#%%\n" 192 | } 193 | } 194 | }, 195 | { 196 | "cell_type": "markdown", 197 | "source": [ 198 | "## 4. index_change\n", 199 | "回答某两个年度相比,它们俩之间相差哪些指标。" 200 | ], 201 | "metadata": { 202 | "collapsed": false 203 | } 204 | }, 205 | { 206 | "cell_type": "code", 207 | "execution_count": 8, 208 | "outputs": [ 209 | { 210 | "data": { 211 | "text/plain": "'2011年与2013年相比,未统计16个指标:飞机多成员机组驾驶员执照、飞机商用驾驶员执照、直升机驾驶员执照、非正常执行计划航班、全部航空公司因其他非正常执行原因、恢复执行机场、主要航空公司非正常执行计划航班、人为责任原因事故征候、全行业取得驾驶执照飞行员、竣工空管项目、其他航空器驾驶员执照、全部航空公司因天气原因非正常执行、飞机航线运输驾驶员执照、全部航空公司因航空公司自身原因非正常执行、飞机私用驾驶员执照、全部航空公司因流量控制非正常执行;2013年与2011年相比,未统计19个指标:续建空管项目、民航全行业应缴税金、通用航空器失踪、新开工空管项目、重大运输任务、无效投诉、全行业客公里收入水平、飞行、机务、空管三个民航特有专业计划招生、赴海外紧急撤侨任务、中小航空公司因航空公司自身原因非正常执行、中小航空公司正常执行计划航班、抢险救灾任务、中小航空公司因其他非正常执行原因、中小航空公司计划航班、中小航空公司因天气原因非正常执行、中小航空公司因流量控制非正常执行、通用航空一般飞行事故、有效投诉、全行业运输收入水平。'" 212 | }, 213 | "execution_count": 8, 214 | "metadata": {}, 215 | "output_type": "execute_result" 216 | } 217 | ], 218 | "source": [ 219 | "chatbot.query('13年比前年增加了哪些指标?')" 220 | ], 221 | "metadata": { 222 | "collapsed": false, 223 | "pycharm": { 224 | "name": "#%%\n" 225 | } 226 | } 227 | }, 228 | { 229 | "cell_type": "code", 230 | "execution_count": 9, 231 | "outputs": [ 232 | { 233 | "data": { 234 | "text/plain": "'2011年与2012年相比,未统计7个指标:非正常执行计划航班、全部航空公司因其他非正常执行原因、主要航空公司非正常执行计划航班、全行业取得驾驶执照飞行员、全部航空公司因天气原因非正常执行、全部航空公司因航空公司自身原因非正常执行、全部航空公司因流量控制非正常执行;2012年与2011年相比,未统计13个指标:中小航空公司计划航班、中小航空公司因天气原因非正常执行、续建空管项目、中小航空公司因流量控制非正常执行、中小航空公司正常执行计划航班、无效投诉、严重事故征候万时率、抢险救灾任务、通用航空器失踪、有效投诉、中小航空公司因其他非正常执行原因、赴海外紧急撤侨任务、中小航空公司因航空公司自身原因非正常执行。'" 235 | }, 236 | "execution_count": 9, 237 | "metadata": {}, 238 | "output_type": "execute_result" 239 | } 240 | ], 241 | "source": [ 242 | "chatbot.query('12年与11年相比,指标变化如何?')" 243 | ], 244 | "metadata": { 245 | "collapsed": false, 246 | "pycharm": { 247 | "name": "#%%\n" 248 | } 249 | } 250 | }, 251 | { 252 | "cell_type": "markdown", 253 | "source": [ 254 | "## 5. exist_catalog\n", 255 | "回答某个年度都统计或记录了哪些目录。" 256 | ], 257 | "metadata": { 258 | "collapsed": false 259 | } 260 | }, 261 | { 262 | "cell_type": "code", 263 | "execution_count": 10, 264 | "outputs": [ 265 | { 266 | "data": { 267 | "text/plain": "'2011年目录包括: 社会责任,固定资产投资,通用航空,经济效益,运输航空,教育及科技,航空安全,航空服务,运输效率与收入。'" 268 | }, 269 | "execution_count": 10, 270 | "metadata": {}, 271 | "output_type": "execute_result" 272 | } 273 | ], 274 | "source": [ 275 | "chatbot.query('2011年有哪些目录?')" 276 | ], 277 | "metadata": { 278 | "collapsed": false, 279 | "pycharm": { 280 | "name": "#%%\n" 281 | } 282 | } 283 | }, 284 | { 285 | "cell_type": "code", 286 | "execution_count": 11, 287 | "outputs": [ 288 | { 289 | "data": { 290 | "text/plain": "'2013年目录包括: 经济效益,通用航空,固定资产投资,社会责任,规章发布,运输效率与收入,飞行员数量,工会工作,航空服务,航空安全,教育及科技,运输航空。'" 291 | }, 292 | "execution_count": 11, 293 | "metadata": {}, 294 | "output_type": "execute_result" 295 | } 296 | ], 297 | "source": [ 298 | "chatbot.query('一三年有啥规格?')" 299 | ], 300 | "metadata": { 301 | "collapsed": false, 302 | "pycharm": { 303 | "name": "#%%\n" 304 | } 305 | } 306 | }, 307 | { 308 | "cell_type": "markdown", 309 | "source": [ 310 | "## 6. begin_stats\n", 311 | "回答某项指标是哪一年开始统计的。" 312 | ], 313 | "metadata": { 314 | "collapsed": false 315 | } 316 | }, 317 | { 318 | "cell_type": "code", 319 | "execution_count": 12, 320 | "outputs": [ 321 | { 322 | "data": { 323 | "text/plain": "'指标“直升机驾驶员执照”最早于2013年开始统计。'" 324 | }, 325 | "execution_count": 12, 326 | "metadata": {}, 327 | "output_type": "execute_result" 328 | } 329 | ], 330 | "source": [ 331 | "chatbot.query('哪年才开始统计直升机驾驶员执照这项指标?')" 332 | ], 333 | "metadata": { 334 | "collapsed": false, 335 | "pycharm": { 336 | "name": "#%%\n" 337 | } 338 | } 339 | }, 340 | { 341 | "cell_type": "code", 342 | "execution_count": 13, 343 | "outputs": [ 344 | { 345 | "data": { 346 | "text/plain": "'指标“航空公司实现营业收入”最早于2011年开始统计。'" 347 | }, 348 | "execution_count": 13, 349 | "metadata": {}, 350 | "output_type": "execute_result" 351 | } 352 | ], 353 | "source": [ 354 | "chatbot.query('在哪一年最先出现了航空公司营业收入数据?')" 355 | ], 356 | "metadata": { 357 | "collapsed": false, 358 | "pycharm": { 359 | "name": "#%%\n" 360 | } 361 | } 362 | } 363 | ], 364 | "metadata": { 365 | "kernelspec": { 366 | "display_name": "Python 3", 367 | "language": "python", 368 | "name": "python3" 369 | }, 370 | "language_info": { 371 | "codemirror_mode": { 372 | "name": "ipython", 373 | "version": 2 374 | }, 375 | "file_extension": ".py", 376 | "mimetype": "text/x-python", 377 | "name": "python", 378 | "nbconvert_exporter": "python", 379 | "pygments_lexer": "ipython2", 380 | "version": "2.7.6" 381 | } 382 | }, 383 | "nbformat": 4, 384 | "nbformat_minor": 0 385 | } -------------------------------------------------------------------------------- /demo/demo3.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "metadata": {}, 7 | "outputs": [ 8 | { 9 | "name": "stdout", 10 | "output_type": "stream", 11 | "text": [ 12 | "欢迎与小航对话,请问有什么可以帮助您的?\n" 13 | ] 14 | } 15 | ], 16 | "source": [ 17 | "import os\n", 18 | "from chatbot import CAChatBot\n", 19 | "\n", 20 | "os.chdir(os.path.join(os.getcwd(), '..'))\n", 21 | "chatbot = CAChatBot()" 22 | ] 23 | }, 24 | { 25 | "cell_type": "markdown", 26 | "metadata": { 27 | "pycharm": { 28 | "name": "#%% md\n" 29 | } 30 | }, 31 | "source": [ 32 | "### 在这个demo中,你将看到如下几个问题类型的回答和介绍:\n", 33 | "1. area_value\n", 34 | "2. area_overall\n", 35 | "3. area_2_overall\n", 36 | "4. areas_m_compare\n", 37 | "5. areas_n_compare\n", 38 | "6. areas_g_compare\n", 39 | "7. areas_2m_compare\n", 40 | "8. areas_2n_compare" 41 | ] 42 | }, 43 | { 44 | "cell_type": "markdown", 45 | "metadata": {}, 46 | "source": [ 47 | "## 1. area_value\n", 48 | "回答某一个年度某些(一或多个)地区的某些(一或多个)指标的值。" 49 | ] 50 | }, 51 | { 52 | "cell_type": "code", 53 | "execution_count": 2, 54 | "metadata": { 55 | "pycharm": { 56 | "name": "#%%\n" 57 | } 58 | }, 59 | "outputs": [ 60 | { 61 | "data": { 62 | "text/plain": [ 63 | "'国内航线运输总周转量为380.61亿吨公里。'" 64 | ] 65 | }, 66 | "execution_count": 2, 67 | "metadata": {}, 68 | "output_type": "execute_result" 69 | } 70 | ], 71 | "source": [ 72 | "chatbot.query('11年国内运输总周转量是多少?')" 73 | ] 74 | }, 75 | { 76 | "cell_type": "code", 77 | "execution_count": 3, 78 | "metadata": { 79 | "pycharm": { 80 | "name": "#%%\n" 81 | } 82 | }, 83 | "outputs": [ 84 | { 85 | "data": { 86 | "text/plain": [ 87 | "'国际航线运输总周转量为194.49亿吨公里;国际货邮周转量,无数据记录。'" 88 | ] 89 | }, 90 | "execution_count": 3, 91 | "metadata": {}, 92 | "output_type": "execute_result" 93 | } 94 | ], 95 | "source": [ 96 | "chatbot.query('12年国际方面,运输总周转量和货邮周转量是多少?')" 97 | ] 98 | }, 99 | { 100 | "cell_type": "code", 101 | "execution_count": 4, 102 | "metadata": { 103 | "pycharm": { 104 | "name": "#%%\n" 105 | } 106 | }, 107 | "outputs": [ 108 | { 109 | "data": { 110 | "text/plain": [ 111 | "'国内航线运输总周转量为461.05亿吨公里;国内航线旅客运输量为32742.0万人次;国际航线运输总周转量为210.68亿吨公里;国际航线旅客运输量为2655.0万人次。'" 112 | ] 113 | }, 114 | "execution_count": 4, 115 | "metadata": {}, 116 | "output_type": "execute_result" 117 | } 118 | ], 119 | "source": [ 120 | "chatbot.query('13年国内和国际方面运输总周转量和旅客运输量各是多少?')" 121 | ] 122 | }, 123 | { 124 | "cell_type": "markdown", 125 | "metadata": {}, 126 | "source": [ 127 | "## 2. area_overall\n", 128 | "回答了某一个年度某些(一或多个)地区的某些(一或多个)指标占其父级地区该指标的比重(例)关系。" 129 | ] 130 | }, 131 | { 132 | "cell_type": "code", 133 | "execution_count": 5, 134 | "metadata": { 135 | "pycharm": { 136 | "name": "#%%\n" 137 | } 138 | }, 139 | "outputs": [ 140 | { 141 | "data": { 142 | "text/plain": [ 143 | "'港澳台航线的运输总周转量为12.64亿吨公里,其占国内运输总周转量的3.32%,国内运输总周转量是其的30.112倍。'" 144 | ] 145 | }, 146 | "execution_count": 5, 147 | "metadata": {}, 148 | "output_type": "execute_result" 149 | } 150 | ], 151 | "source": [ 152 | "chatbot.query('11年港澳台运输总周转量占其父级的百分之几?')" 153 | ] 154 | }, 155 | { 156 | "cell_type": "code", 157 | "execution_count": 6, 158 | "metadata": { 159 | "pycharm": { 160 | "name": "#%%\n" 161 | } 162 | }, 163 | "outputs": [ 164 | { 165 | "data": { 166 | "text/plain": [ 167 | "'港澳台航线的运输总周转量为13.66亿吨公里,其占国内运输总周转量的3.28%,国内运输总周转量是其的30.441倍;港澳台无旅客运输量父级地区的数据记录,无法比较。'" 168 | ] 169 | }, 170 | "execution_count": 6, 171 | "metadata": {}, 172 | "output_type": "execute_result" 173 | } 174 | ], 175 | "source": [ 176 | "chatbot.query('12年港澳台和国际的运输总周转量和游客运输量是父级的多少倍?')" 177 | ] 178 | }, 179 | { 180 | "cell_type": "markdown", 181 | "metadata": { 182 | "pycharm": { 183 | "name": "#%% md\n" 184 | } 185 | }, 186 | "source": [ 187 | "## 3. area_2_overall\n", 188 | "回答了某两个年度中某一个地区的某项指标占其父级地区该指标的比重变化关系。" 189 | ] 190 | }, 191 | { 192 | "cell_type": "code", 193 | "execution_count": 7, 194 | "metadata": { 195 | "pycharm": { 196 | "name": "#%%\n" 197 | } 198 | }, 199 | "outputs": [ 200 | { 201 | "data": { 202 | "text/plain": [ 203 | "'2012年港澳台旅客运输量为834.0万人次,其总体地区(国内)的为29600.0万人次,约占总体的2.82%;2011年港澳台旅客运输量为760.0万人次,其总体地区(国内)的为27199.0万人次,约占总体的2.79%;前者相比后者提高0.03%。'" 204 | ] 205 | }, 206 | "execution_count": 7, 207 | "metadata": {}, 208 | "output_type": "execute_result" 209 | } 210 | ], 211 | "source": [ 212 | "chatbot.query('12年港澳台的游客运输量占总体的百分比比去年变化了?')" 213 | ] 214 | }, 215 | { 216 | "cell_type": "code", 217 | "execution_count": 8, 218 | "metadata": { 219 | "pycharm": { 220 | "name": "#%%\n" 221 | } 222 | }, 223 | "outputs": [ 224 | { 225 | "data": { 226 | "text/plain": [ 227 | "'无2013、2011这几年中部地区旅客吞吐量的父级数据记录,无法比较。'" 228 | ] 229 | }, 230 | "execution_count": 8, 231 | "metadata": {}, 232 | "output_type": "execute_result" 233 | } 234 | ], 235 | "source": [ 236 | "chatbot.query('13年中部地区旅客吞吐量的父级记录,相比11变化了多少?')" 237 | ] 238 | }, 239 | { 240 | "cell_type": "markdown", 241 | "metadata": { 242 | "pycharm": { 243 | "name": "#%% md\n" 244 | } 245 | }, 246 | "source": [ 247 | "## 4. areas_m_compare\n", 248 | "回答了某一个年度中,对于某项(一或多个)指标,某一个地区与另外一个地区的倍数关系。" 249 | ] 250 | }, 251 | { 252 | "cell_type": "code", 253 | "execution_count": 9, 254 | "metadata": { 255 | "pycharm": { 256 | "name": "#%%\n" 257 | } 258 | }, 259 | "outputs": [ 260 | { 261 | "data": { 262 | "text/plain": [ 263 | "'国内航线的货邮运输量为406.7万吨,国际航线的货邮运输量为154.5万吨,前者是后者的2.632倍,后者是前者的0.38倍。'" 264 | ] 265 | }, 266 | "execution_count": 9, 267 | "metadata": {}, 268 | "output_type": "execute_result" 269 | } 270 | ], 271 | "source": [ 272 | "chatbot.query('13年国内的货邮运输量是国际的几倍?')" 273 | ] 274 | }, 275 | { 276 | "cell_type": "code", 277 | "execution_count": 10, 278 | "metadata": { 279 | "pycharm": { 280 | "name": "#%%\n" 281 | } 282 | }, 283 | "outputs": [ 284 | { 285 | "data": { 286 | "text/plain": [ 287 | "'港澳台航线的运输总周转量为12.64亿吨公里,国际航线的运输总周转量为196.84亿吨公里,前者是后者的0.064倍,后者是前者的15.573倍;无港澳台旅客周转量数据记录,无法比较。'" 288 | ] 289 | }, 290 | "execution_count": 10, 291 | "metadata": {}, 292 | "output_type": "execute_result" 293 | } 294 | ], 295 | "source": [ 296 | "chatbot.query('11年港澳台运输总周转量和游客周转量是国际的多少倍?')" 297 | ] 298 | }, 299 | { 300 | "cell_type": "code", 301 | "execution_count": 11, 302 | "metadata": { 303 | "pycharm": { 304 | "name": "#%%\n" 305 | } 306 | }, 307 | "outputs": [ 308 | { 309 | "data": { 310 | "text/plain": [ 311 | "'无港澳台货邮周转量数据记录,无法比较。'" 312 | ] 313 | }, 314 | "execution_count": 11, 315 | "metadata": {}, 316 | "output_type": "execute_result" 317 | } 318 | ], 319 | "source": [ 320 | "chatbot.query('12年港澳台的货邮周转量是国内的多少倍?')" 321 | ] 322 | }, 323 | { 324 | "cell_type": "markdown", 325 | "metadata": {}, 326 | "source": [ 327 | "## 5. areas_n_compare\n", 328 | "回答了某一个年度中,对于某项(一或多个)指标,某一个地区与另外一个地区的数值关系(比较关系)。" 329 | ] 330 | }, 331 | { 332 | "cell_type": "code", 333 | "execution_count": 12, 334 | "metadata": { 335 | "pycharm": { 336 | "name": "#%%\n" 337 | } 338 | }, 339 | "outputs": [ 340 | { 341 | "data": { 342 | "text/plain": [ 343 | "'港澳台航线的旅客运输量为760.0万人次,国内航线的旅客运输量为27199.0万人次,前者比后者少26439.0万人次。'" 344 | ] 345 | }, 346 | "execution_count": 12, 347 | "metadata": {}, 348 | "output_type": "execute_result" 349 | } 350 | ], 351 | "source": [ 352 | "chatbot.query('11年港澳台游客运输量比国内的少多少?')" 353 | ] 354 | }, 355 | { 356 | "cell_type": "code", 357 | "execution_count": 13, 358 | "metadata": { 359 | "pycharm": { 360 | "name": "#%%\n" 361 | } 362 | }, 363 | "outputs": [ 364 | { 365 | "data": { 366 | "text/plain": [ 367 | "'国内航线的货邮运输量为388.5万吨,国际航线的货邮运输量为156.5万吨,前者比后者多232.0万吨;无国内旅客周转量数据记录,无法比较。'" 368 | ] 369 | }, 370 | "execution_count": 13, 371 | "metadata": {}, 372 | "output_type": "execute_result" 373 | } 374 | ], 375 | "source": [ 376 | "chatbot.query('12年国内游客周转量和货邮运输量比国际的变化了多少?')" 377 | ] 378 | }, 379 | { 380 | "cell_type": "markdown", 381 | "metadata": { 382 | "pycharm": { 383 | "name": "#%% md\n" 384 | } 385 | }, 386 | "source": [ 387 | "## 6. areas_g_compare\n", 388 | "回答了某一个年度的某一个地区的某项(一或多个)指标相比去年(同比变化只可以与去年相比)的同比变化关系。" 389 | ] 390 | }, 391 | { 392 | "cell_type": "code", 393 | "execution_count": 14, 394 | "metadata": { 395 | "pycharm": { 396 | "name": "#%%\n" 397 | } 398 | }, 399 | "outputs": [ 400 | { 401 | "data": { 402 | "text/plain": [ 403 | "'2012年的国内货邮运输量为388.5万吨,其去年的为379.4万吨,同比增长2.4%;无2012年关于国内旅客周转量的数据。'" 404 | ] 405 | }, 406 | "execution_count": 14, 407 | "metadata": {}, 408 | "output_type": "execute_result" 409 | } 410 | ], 411 | "source": [ 412 | "chatbot.query('12年国内游客周转量和货邮运输量同比变化多少?')" 413 | ] 414 | }, 415 | { 416 | "cell_type": "markdown", 417 | "metadata": {}, 418 | "source": [ 419 | "## 7. areas_2m_compare\n", 420 | "回答了某两个年度中某(一或多个)地区的某一个指标之间的倍数变化关系。" 421 | ] 422 | }, 423 | { 424 | "cell_type": "code", 425 | "execution_count": 15, 426 | "metadata": { 427 | "pycharm": { 428 | "name": "#%%\n" 429 | } 430 | }, 431 | "outputs": [ 432 | { 433 | "data": { 434 | "text/plain": [ 435 | "'2011年的港澳台运输总周转量(12.64亿吨公里)是2012年的(13.66亿吨公里)0.925倍。'" 436 | ] 437 | }, 438 | "execution_count": 15, 439 | "metadata": {}, 440 | "output_type": "execute_result" 441 | } 442 | ], 443 | "source": [ 444 | "chatbot.query('11年港澳台运输总周转量和旅客吞吐量是12年的多少倍?')" 445 | ] 446 | }, 447 | { 448 | "cell_type": "code", 449 | "execution_count": 16, 450 | "metadata": { 451 | "pycharm": { 452 | "name": "#%%\n" 453 | } 454 | }, 455 | "outputs": [ 456 | { 457 | "data": { 458 | "text/plain": [ 459 | "'无关于2013年的国内货邮周转量的记录;无关于2013年的国际货邮周转量的记录。'" 460 | ] 461 | }, 462 | "execution_count": 16, 463 | "metadata": {}, 464 | "output_type": "execute_result" 465 | } 466 | ], 467 | "source": [ 468 | "chatbot.query('13年国内与国际货邮周转量占11年的百分之几?')" 469 | ] 470 | }, 471 | { 472 | "cell_type": "markdown", 473 | "metadata": {}, 474 | "source": [ 475 | "## 8. areas_2n_compare\n", 476 | "回答了某两个年度中某(一或多个)地区的某一个指标之间的数值关系(比较关系)。" 477 | ] 478 | }, 479 | { 480 | "cell_type": "code", 481 | "execution_count": 17, 482 | "metadata": { 483 | "pycharm": { 484 | "name": "#%%\n" 485 | } 486 | }, 487 | "outputs": [ 488 | { 489 | "data": { 490 | "text/plain": [ 491 | "'2013年的国内运输总周转量(461.05亿吨公里)比2012年的(415.83亿吨公里)增加45.22亿吨公里。'" 492 | ] 493 | }, 494 | "execution_count": 17, 495 | "metadata": {}, 496 | "output_type": "execute_result" 497 | } 498 | ], 499 | "source": [ 500 | "chatbot.query('13年国内运输总周转量比12年多多少?')" 501 | ] 502 | }, 503 | { 504 | "cell_type": "code", 505 | "execution_count": 18, 506 | "metadata": { 507 | "pycharm": { 508 | "name": "#%%\n" 509 | } 510 | }, 511 | "outputs": [ 512 | { 513 | "data": { 514 | "text/plain": [ 515 | "'无关于2012年的北上广旅客吞吐量的记录。'" 516 | ] 517 | }, 518 | "execution_count": 18, 519 | "metadata": {}, 520 | "output_type": "execute_result" 521 | } 522 | ], 523 | "source": [ 524 | "chatbot.query('12年的北上广与一年前相比,旅客吞吐量降低多少?')\n" 525 | ] 526 | } 527 | ], 528 | "metadata": { 529 | "kernelspec": { 530 | "display_name": "Python 3", 531 | "language": "python", 532 | "name": "python3" 533 | }, 534 | "language_info": { 535 | "codemirror_mode": { 536 | "name": "ipython", 537 | "version": 3 538 | }, 539 | "file_extension": ".py", 540 | "mimetype": "text/x-python", 541 | "name": "python", 542 | "nbconvert_exporter": "python", 543 | "pygments_lexer": "ipython3", 544 | "version": "3.7.7" 545 | } 546 | }, 547 | "nbformat": 4, 548 | "nbformat_minor": 1 549 | } 550 | -------------------------------------------------------------------------------- /question_classifier.py: -------------------------------------------------------------------------------- 1 | # 问题分类器 2 | import ahocorasick 3 | 4 | from lib.check import * 5 | from lib.regexp import * 6 | from lib.result import Result 7 | from lib.utils import read_words, debug 8 | from lib.complement import year_complement, index_complement 9 | from lib.errors import QuestionOrderError 10 | 11 | 12 | class QuestionClassifier: 13 | 14 | def __init__(self): 15 | # 词根目录 16 | self.region_wds_root = './data/dicts/{}.txt' 17 | self.qwds_root = './data/question/{}.txt' 18 | self.rwds_root = './data/reference/{}.txt' 19 | self.twds_root = './data/tail/{}.txt' 20 | 21 | self.word_type_dict = {} 22 | # 特征词 23 | self.area_wds = self.read_region_words('area') 24 | self.catalog_wds = self.read_region_words('catalog') 25 | self.index_wds = self.read_region_words('index') 26 | self.year_wds = self.read_region_words('year') 27 | 28 | # 问句疑问词 29 | self.exist_qwds = read_words(self.qwds_root.format('Exist')) 30 | self.value_qwds = read_words(self.qwds_root.format('Value')) 31 | self.when_qwds = read_words(self.qwds_root.format('When')) 32 | 33 | # 指代词 34 | self.status_rwds = read_words(self.rwds_root.format('Status')) 35 | self.catalog_rwds = read_words(self.rwds_root.format('Catalog')) 36 | self.parent_index_rwds = read_words(self.rwds_root.format('ParentIndex')) 37 | self.child_index_rwds = read_words(self.rwds_root.format('ChildIndex')) 38 | self.location_rwds = read_words(self.rwds_root.format('Location')) 39 | self.index_rwds = read_words(self.rwds_root.format('Index')) 40 | self.max_rwds = read_words(self.rwds_root.format('Max')) 41 | 42 | # 尾词 43 | self.is_twds = read_words(self.twds_root.format('Is')) 44 | 45 | self.region_wds = set(self.area_wds + self.catalog_wds + self.index_wds + self.year_wds) 46 | self.region_tree = self.build_actree() 47 | 48 | def read_region_words(self, word_type: str) -> list: 49 | """ 加载特征词并构建特征词类型字典 """ 50 | with open(self.region_wds_root.format(word_type.capitalize()), encoding='utf-8') as f: 51 | collect = [] 52 | for word in f: 53 | word = word.strip('\n') 54 | self.word_type_dict[word] = word_type 55 | collect.append(word) 56 | return collect 57 | 58 | def build_actree(self): 59 | actree = ahocorasick.Automaton() 60 | for i, word in enumerate(self.region_wds): 61 | actree.add_word(word, (i, word)) 62 | actree.make_automaton() 63 | return actree 64 | 65 | def question_filter(self, question: str) -> Result: 66 | question = question.replace(' ', '') 67 | # 过滤年份 68 | filtered_question = year_complement(question) 69 | # 过滤特征词 70 | region_wds = [] 71 | for w in self.region_tree.iter(filtered_question): 72 | region_wds.append(w[1][1]) 73 | region_dict = {w: self.word_type_dict.get(w) for w in region_wds} 74 | 75 | return Result(region_dict, question, filtered_question) 76 | 77 | def extract_index(self, result: Result, len_threshold: int = 4, ratio_threshold: float = 0.5): 78 | """ 提取因错别字或说法而未识别到的指标 """ 79 | new_word, old_word = index_complement(result.filtered_question, self.index_wds, len_threshold, ratio_threshold) 80 | if new_word: 81 | debug('||REPLACE FOUND||', new_word, '<=', old_word) 82 | result.add_word(new_word, self.word_type_dict.get(new_word)) 83 | result.replace_words(old_word, new_word) 84 | 85 | def classify(self, question: str): 86 | result = self.question_filter(question) 87 | if result.count('index') == 0 and 'catalog' not in result: 88 | self.extract_index(result) 89 | # 没有任何提取结果 90 | if result.is_wds_null(): 91 | return None 92 | self._classify_tree(result) 93 | debug('||QUESTION WORDS||', result.region_wds) 94 | debug('||QUESTION TYPES||', result.question_types) 95 | return result 96 | 97 | def _classify_tree(self, result: Result): 98 | # 收集实体类型 99 | question = result.filtered_question 100 | year_count = result.count('year') 101 | 102 | # 问题与单个年份相关 103 | if year_count == 1: 104 | # 全年总体情况 105 | if check_contain(self.status_rwds, question) and 'year' in result and len(result) == 1: 106 | result.add_qtype('year_status') 107 | # 全年含有目录 108 | if check_contain(self.exist_qwds, question) and check_contain(self.catalog_rwds, question): 109 | result.add_qtype('exist_catalog') 110 | 111 | # 目录 112 | if 'catalog' in result: 113 | # 总体情况 114 | if check_contain(self.status_rwds, question): 115 | result.add_qtype('catalog_status') 116 | 117 | # 指标 118 | if 'index' in result: 119 | # 值 120 | if check_contain(self.value_qwds, question) or check_endswith(self.is_twds, question): 121 | if not check_contain(self.child_index_rwds, question): 122 | # 涉及地区 123 | if 'area' in result: 124 | result.add_qtype('area_value') 125 | else: 126 | result.add_qtype('index_value') 127 | # 值比较(上级) 128 | if check_regexp(question, MultipleCmp1, 129 | functions=[lambda x: check_contain(self.parent_index_rwds, x[0][-1])], 130 | callback=lambda x: QuestionOrderError.check(x, self.parent_index_rwds) 131 | ): 132 | # 涉及地区 133 | if 'area' in result: 134 | result.add_qtype('area_overall') 135 | else: 136 | result.add_qtype('index_overall') 137 | # 值比较(同类同单位) 138 | if result.count('index') < 2: 139 | self.extract_index(result, ratio_threshold=0.7) 140 | question = result.filtered_question # 重新查询后更新 141 | if result.count('index') == 2 and 'area' not in result: 142 | if check_regexp(question, MultipleCmp1, functions=[ 143 | lambda x: check_list_contain(result['index'], x[0], 0, -1) 144 | ]): 145 | result.add_qtype('indexes_m_compare') # 比较倍数关系 146 | if check_regexp(question, NumberCmp1, functions=[ 147 | lambda x: (check_list_contain(result['index'], x[0], 0, -1) or 148 | check_all_contain(result['index'], x[0][0])) 149 | ]): 150 | result.add_qtype('indexes_n_compare') # 比较数量关系 151 | # 地区值比较(相同指标不同地区) 152 | if result.count('area') == 2: 153 | if check_regexp(question, MultipleCmp1, functions=[ 154 | lambda x: (check_list_contain(result['area'], x[0], 0, -1) and 155 | check_list_contain(result['index'], x[0], 0, not_=-1)) 156 | ]): 157 | result.add_qtype('areas_m_compare') # 比较倍数关系 158 | if check_regexp(question, NumberCmp1, functions=[ 159 | lambda x: ((check_list_contain(result['area'], x[0], 0, -1) and 160 | check_contain(result['index'], x[0][0])) 161 | or 162 | (check_all_contain(result['area'], x[0][0]) and 163 | check_list_any_contain(result['index'], x[0], 0, -1))) 164 | ]): 165 | result.add_qtype('areas_n_compare') # 比较数量关系 166 | # 同比值比较 167 | if check_regexp(question, GrowthCmp, functions=[ 168 | lambda x: check_all_contain(result['index'], x[0]) 169 | ]): 170 | if 'area' in result: 171 | # 单地区多指标 172 | if result.count('area') == 1: 173 | result.add_qtype('areas_g_compare') 174 | else: 175 | result.add_qtype('indexes_g_compare') 176 | # 指标的组成 177 | if check_contain(self.child_index_rwds, question): 178 | result.add_qtype('index_compose') 179 | 180 | # 问题与两个年份相关 181 | elif year_count == 2: 182 | # 目录与指标的变化情况 183 | if result.count('year') == len(result): 184 | if check_contain(self.catalog_rwds, question): 185 | result.add_qtype('catalog_change') 186 | elif check_contain(self.index_rwds, question): 187 | result.add_qtype('index_change') 188 | 189 | # 指标 190 | if 'index' in result: 191 | if check_contain(self.parent_index_rwds, question): 192 | # 上级占比变化 193 | if check_regexp(question, NumberCmp2[0], NumberCmp2[1], functions=[ 194 | lambda x: check_contain(self.parent_index_rwds, x[0]) 195 | ]*2): 196 | if 'area' not in result: 197 | result.add_qtype('index_2_overall') 198 | elif check_regexp(question, NumberCmp2[0], NumberCmp2[1], functions=[ 199 | lambda x: check_contain(result['area'], x[0]) 200 | ]*2): 201 | result.add_qtype('area_2_overall') 202 | else: 203 | # 比较数值 204 | if check_regexp(question, *NumberCmp2, functions=[ 205 | lambda x: check_contain(result['index'], x[0]), 206 | lambda x: check_contain(result['index'], x[0]), 207 | lambda x: check_contain(result['index'], x[0]), 208 | lambda x: check_contain(result['index'], x[0][-1]) 209 | ]): 210 | if 'area' not in result: # 不涉及地区 211 | result.add_qtype('indexes_2n_compare') 212 | else: # 涉及地区 213 | if result.count('index') == 1: # 单指标下不同地区比较 214 | if check_regexp(question, *NumberCmp2, functions=[ 215 | lambda x: check_contain(result['area'], x[0]), 216 | lambda x: check_contain(result['area'], x[0]), 217 | lambda x: check_contain(result['area'], x[0]), 218 | lambda x: check_contain(result['area'], x[0][0]), 219 | ]): 220 | result.add_qtype('areas_2n_compare') 221 | # 比较倍数 222 | if check_regexp(question, MultipleCmp2, functions=[ 223 | lambda x: check_list_any_contain(result['index'], x[0], 0, -1) 224 | ]): 225 | if 'area' not in result: # 不涉及地区 226 | result.add_qtype('indexes_2m_compare') 227 | else: # 涉及地区 228 | if check_regexp(question, MultipleCmp2, functions=[ 229 | lambda x: check_list_any_contain(result['area'], x[0], 0, -1) 230 | ]): 231 | result.add_qtype('areas_2m_compare') 232 | 233 | # 问题与多个年份相关 234 | elif year_count > 2: 235 | # 指标/目录变化趋势 236 | if result.count('year') == len(result) and check_contain(self.status_rwds, question): 237 | if check_contain(self.catalog_rwds, question): 238 | result.add_qtype('catalogs_change') 239 | elif check_contain(self.index_rwds, question): 240 | result.add_qtype('indexes_change') 241 | 242 | # 关于指标的变化趋势 243 | if 'index' in result: 244 | # 占上级的 245 | if check_regexp(question, MultipleCmp1, functions=[ 246 | lambda x: (check_contain(result['index'], x[0][0]) and 247 | check_contain(self.status_rwds, x[0][-1]) and 248 | check_contain(self.parent_index_rwds, x[0][-1])) 249 | ]): 250 | if 'area' in result: 251 | result.add_qtype('areas_overall_trend') 252 | else: 253 | result.add_qtype('indexes_overall_trend') 254 | # 值的 255 | if check_contain(self.status_rwds, question) and not check_contain(self.parent_index_rwds, question): 256 | if 'area' in result: 257 | result.add_qtype('areas_trend') 258 | else: 259 | result.add_qtype('indexes_trend') 260 | # 最值 261 | if check_contain(self.max_rwds, question): 262 | if 'area' in result: 263 | result.add_qtype('areas_max') 264 | else: 265 | result.add_qtype('indexes_max') 266 | 267 | # 问题与年份无关 268 | else: 269 | if 'index' in result and check_contain(self.when_qwds, question): 270 | result.add_qtype('begin_stats') 271 | -------------------------------------------------------------------------------- /test/classifier_test.py: -------------------------------------------------------------------------------- 1 | import os 2 | import unittest 3 | 4 | from question_classifier import QuestionClassifier 5 | from lib.errors import QuestionError 6 | 7 | os.chdir(os.path.join(os.getcwd(), '..')) 8 | 9 | 10 | class QCTest(unittest.TestCase): 11 | 12 | qc = QuestionClassifier() 13 | 14 | def check_question(self, question: str): 15 | try: 16 | res = self.qc.classify(question).question_types 17 | return res if res else [] 18 | except QuestionError: 19 | return [] 20 | 21 | # 年度发展状况 22 | def test_year_status(self): 23 | self.assertEqual(self.check_question('2011年总体情况怎样?'), ['year_status']) 24 | self.assertEqual(self.check_question('2011年发展形势怎样?'), ['year_status']) 25 | self.assertEqual(self.check_question('2011年发展如何?'), ['year_status']) 26 | self.assertEqual(self.check_question('11年形势怎样?'), ['year_status']) 27 | 28 | # 年度某目录总体发展状况 29 | def test_catalog_status(self): 30 | self.assertEqual(self.check_question('2011年运输航空总体情况怎样?'), ['catalog_status']) 31 | self.assertEqual(self.check_question('2011年航空安全发展形势怎样?'), ['catalog_status']) 32 | self.assertEqual(self.check_question('2011年教育及科技发展如何?'), ['catalog_status']) 33 | self.assertEqual(self.check_question('2011固定资产投资形势怎样?'), ['catalog_status']) 34 | 35 | # 对比两年变化的目录 36 | def test_catalog_change(self): 37 | self.assertEqual(self.check_question('12年比11年多了哪些目录'), ['catalog_change']) 38 | self.assertEqual(self.check_question('12年比去年增加了哪些目录'), ['catalog_change']) 39 | self.assertEqual(self.check_question('12年比去年少了哪些标准?'), ['catalog_change']) 40 | self.assertEqual(self.check_question('12年与去年相比,目录变化如何?'), ['catalog_change']) 41 | 42 | # 对比两年变化的指标 43 | def test_index_change(self): 44 | self.assertEqual(self.check_question('12年比11年多了哪些指标'), ['index_change']) 45 | self.assertEqual(self.check_question('12年比去年增加了哪些指标'), ['index_change']) 46 | self.assertEqual(self.check_question('12年比去年少了哪些指标?'), ['index_change']) 47 | self.assertEqual(self.check_question('12年与去年相比,指标变化如何?'), ['index_change']) 48 | 49 | # 年度总体目录包括 50 | def test_exist_catalog(self): 51 | self.assertEqual(self.check_question('2011年有哪些指标目录?'), ['exist_catalog']) 52 | self.assertEqual(self.check_question('2011年有哪些基准?'), ['exist_catalog']) 53 | self.assertEqual(self.check_question('2011年有啥规格?'), ['exist_catalog']) 54 | self.assertEqual(self.check_question('2011年的目录有哪些?'), ['exist_catalog']) 55 | 56 | # 指标值 57 | def test_index_value(self): 58 | self.assertEqual(self.check_question('2011年的货邮周转量和游客周转量是多少?'), ['index_value']) 59 | self.assertEqual(self.check_question('2011年的货邮周转量的值是?'), ['index_value']) 60 | self.assertEqual(self.check_question('2011年的货邮周转量为?'), ['index_value']) 61 | self.assertEqual(self.check_question('2011年的货邮周转量是'), ['index_value']) 62 | 63 | # 指标与总指标的比较 64 | def test_index_1_overall(self): 65 | self.assertEqual(self.check_question('2011年的游客周转量占总体多少?'), ['index_overall']) 66 | self.assertEqual(self.check_question('2011年的游客周转量占父指标多少份额?'), ['index_overall']) 67 | self.assertEqual(self.check_question('2011年的游客周转量是总体的多少倍?'), ['index_overall']) 68 | self.assertEqual(self.check_question('2011游客周转量占总体的百分之多少?'), ['index_overall']) 69 | self.assertEqual(self.check_question('2011年的游客周转量为其总体的多少倍?'), ['index_overall']) 70 | self.assertEqual(self.check_question('2011游客周转量占总量的多少?'), ['index_overall']) 71 | self.assertEqual(self.check_question('2011年游客周转量占有总额的多少比例?'), ['index_overall']) 72 | # 反例 73 | self.assertEqual(self.check_question('2011年总体是货邮周转量的百分之几?'), []) 74 | 75 | def test_index_2_overall(self): 76 | self.assertEqual(self.check_question('2012年游客周转量占总体的百分比比去年变化多少?'), ['index_2_overall']) 77 | self.assertEqual(self.check_question('2012年游客周转量占总体的百分比,相比11年变化多少?'), ['index_2_overall']) 78 | self.assertEqual(self.check_question('2012年相比11年,游客周转量占总体的百分比变化多少?'), ['index_2_overall']) 79 | self.assertEqual(self.check_question('2012年的游客周转量占总计比例比去年增加多少?'), ['index_2_overall']) 80 | self.assertEqual(self.check_question('2013年的游客周转量占父级的倍数比11年降低多少?'), ['index_2_overall']) 81 | 82 | # 指标同类之间的比较 83 | def test_indexes_1_compare(self): 84 | # 倍数比较 85 | self.assertEqual(self.check_question('2011年游客周转量是货邮周转量的几倍?'), ['indexes_m_compare']) 86 | self.assertEqual(self.check_question('2011年游客周转量是货邮周转量的百分之几?'), ['indexes_m_compare']) 87 | # 反例 88 | self.assertEqual(self.check_question('2011年总体是货邮周转量的几倍?'), []) 89 | self.assertEqual(self.check_question('2011年货邮周转量是货邮周转量的几倍?'), []) 90 | 91 | # 数量比较 92 | self.assertEqual(self.check_question('11年旅客周转量比货邮周转量多多少?'), ['indexes_n_compare']) 93 | self.assertEqual(self.check_question('11年旅客周转量比货邮周转量大?'), ['indexes_n_compare']) 94 | self.assertEqual(self.check_question('11年旅客周转量比货邮周转量少多少?'), ['indexes_n_compare']) 95 | self.assertEqual(self.check_question('11年旅客周转量比货邮周转量增加了多少?'), ['indexes_n_compare']) 96 | self.assertEqual(self.check_question('11年旅客周转量比货邮周转量降低了?'), ['indexes_n_compare']) 97 | self.assertEqual(self.check_question('11年旅客周转量比货邮周转量降低了?'), ['indexes_n_compare']) 98 | self.assertEqual(self.check_question('11年旅客周转量比货邮周转量变化了多少?'), ['indexes_n_compare']) 99 | self.assertEqual(self.check_question('11年旅客周转量比货邮周转量变了?'), ['indexes_n_compare']) 100 | self.assertEqual(self.check_question('11年旅客周转量与货邮周转量相比降低了多少?'), ['indexes_n_compare']) 101 | self.assertEqual(self.check_question('11年旅客周转量与货邮周转量比,降低了多少?'), ['indexes_n_compare']) 102 | self.assertEqual(self.check_question('11年旅客周转量与货邮周转量比较 降低了多少?'), ['indexes_n_compare']) 103 | # 反例 104 | self.assertEqual(self.check_question('2011年旅客周转量,货邮周转量比运输总周转量降低了?'), []) 105 | 106 | # 同比变化(只与前一年比较) 107 | self.assertEqual(self.check_question('2012年旅客周转量同比增长多少?'), ['indexes_g_compare']) 108 | self.assertEqual(self.check_question('2012年旅客周转量同比下降百分之几?'), ['indexes_g_compare']) 109 | self.assertEqual(self.check_question('2012年旅客周转量和货邮周转量同比下降百分之几?'), ['indexes_g_compare']) 110 | # 反例 111 | self.assertEqual(self.check_question('2012年旅客周转量同比13年下降百分之几?'), []) 112 | 113 | def test_indexes_2_compare(self): 114 | self.assertEqual(self.check_question('2011年游客周转量是12年的百分之几?'), ['indexes_2m_compare']) 115 | self.assertEqual(self.check_question('2011年的是12年游客周转量的百分之几?'), ['indexes_2m_compare']) 116 | self.assertEqual(self.check_question('2011年游客周转量占12的百分之?'), ['indexes_2m_compare']) 117 | self.assertEqual(self.check_question('2011年游客周转量是12年的几倍?'), ['indexes_2m_compare']) 118 | self.assertEqual(self.check_question('2011年游客周转量为12年的多少倍?'), ['indexes_2m_compare']) 119 | 120 | self.assertEqual(self.check_question('2011年游客周转量比12年降低了?'), ['indexes_2n_compare']) 121 | self.assertEqual(self.check_question('2012年游客周转量比去年增加了?'), ['indexes_2n_compare']) 122 | self.assertEqual(self.check_question('2012年游客周转量比去年多了多少?'), ['indexes_2n_compare']) 123 | self.assertEqual(self.check_question('12年的货邮周转量比去年变化了多少?'), ['indexes_2n_compare']) 124 | self.assertEqual(self.check_question('12年的货邮周转量同去年相比变化了多少?'), ['indexes_2n_compare']) 125 | self.assertEqual(self.check_question('13年的货邮周转量同2年前相比变化了多少?'), ['indexes_2n_compare']) 126 | self.assertEqual(self.check_question('12年同去年相比,货邮周转量变化了多少?'), ['indexes_2n_compare']) 127 | 128 | # 指标的组成 129 | def test_index_compose(self): 130 | self.assertEqual(self.check_question('2011年游客周转量的子集有?'), ['index_compose']) 131 | self.assertEqual(self.check_question('2011年游客周转量的组成?'), ['index_compose']) 132 | self.assertEqual(self.check_question('2011年游客周转量的子指标组成情况?'), ['index_compose']) 133 | 134 | # 地区指标值 135 | def test_area_value(self): 136 | self.assertEqual(self.check_question('11年国内的运输总周转量为?'), ['area_value']) 137 | self.assertEqual(self.check_question('11年国内和国际的运输总周转量为'), ['area_value']) 138 | self.assertEqual(self.check_question('11年国际方面运输总周转量是多少?'), ['area_value']) 139 | 140 | # 地区指标与总指标的比较 141 | def test_area_1_overall(self): 142 | self.assertEqual(self.check_question('11年国内的运输总周转量占总体的百分之几?'), ['area_overall']) 143 | self.assertEqual(self.check_question('11年国际运输总周转量占总值的多少?'), ['area_overall']) 144 | self.assertEqual(self.check_question('11年港澳台运输总周转量是全体的多少倍?'), ['area_overall']) 145 | # 反例 146 | self.assertEqual(self.check_question('11年父级是港澳台运输总周转量的多少倍?'), []) 147 | 148 | def test_area_2_overall(self): 149 | self.assertEqual(self.check_question('2012年国内的游客周转量占总体的百分比比去年变化多少?'), ['area_2_overall']) 150 | self.assertEqual(self.check_question('2012年国际游客周转量占总体的百分比,相比11年变化多少?'), ['area_2_overall']) 151 | self.assertEqual(self.check_question('2012年相比11年,港澳台游客周转量占总体的百分比变化多少?'), ['area_2_overall']) 152 | self.assertEqual(self.check_question('2012年的国内游客周转量占总计比例比去年增加多少?'), ['area_2_overall']) 153 | self.assertEqual(self.check_question('2013年的国际游客周转量占父级的倍数比11年降低多少?'), ['area_2_overall']) 154 | 155 | # 地区指标与地区指标的比较 156 | def test_areas_1_compare(self): 157 | # 倍数比较 158 | self.assertEqual(self.check_question('11年港澳台运输总周转量占国内的百分之几?'), ['areas_m_compare']) 159 | self.assertEqual(self.check_question('11年国内的运输总周转量是港澳台的几倍?'), ['areas_m_compare']) 160 | self.assertEqual(self.check_question('11年国际运输总周转量是国内的多少倍?'), ['areas_m_compare']) 161 | self.assertEqual(self.check_question('11年港澳台运输总周转量是国际的多少倍?'), ['areas_m_compare']) 162 | # 反例 163 | self.assertEqual(self.check_question('11年港澳台运输总周转量是国内游客周转量的多少倍?'), []) 164 | self.assertEqual(self.check_question('11年港澳台是国内游客周转量的多少倍?'), []) 165 | 166 | # 数量比较 167 | self.assertEqual(self.check_question('2011年国内游客周转量比国际多多少?'), ['areas_n_compare']) 168 | self.assertEqual(self.check_question('2011年港澳台游客周转量比国内的少多少?'), ['areas_n_compare']) 169 | self.assertEqual(self.check_question('2011年港澳台游客周转量与国内的相比降低多少?'), ['areas_n_compare']) 170 | self.assertEqual(self.check_question('2011年港澳台与国内的相比游客周转量降低多少?'), ['areas_n_compare']) 171 | # 反例 172 | self.assertEqual(self.check_question('2011年国内比国际游客周转量少了?'), []) 173 | 174 | # 同比变化(只与前一年比较, 单地区多指标) 175 | self.assertEqual(self.check_question('2012年国内游客周转量同比增长了?'), ['areas_g_compare']) 176 | self.assertEqual(self.check_question('2012年国内游客周转量同比下降了多少?'), ['areas_g_compare']) 177 | self.assertEqual(self.check_question('2012年国内游客周转量和货邮周转量同比变化了多少?'), ['areas_g_compare']) 178 | # 反例 179 | self.assertEqual(self.check_question('2012年国内游客周转量和国际货邮周转量同比变化了多少?'), []) 180 | self.assertEqual(self.check_question('2012年国内游客周转量同比13年变化了多少?'), []) 181 | 182 | def test_areas_2_compare(self): 183 | self.assertEqual(self.check_question('11年港澳台运输总周转量是12年的多少倍?'), ['areas_2m_compare']) 184 | self.assertEqual(self.check_question('12年的是11年港澳台运输总周转量的多少倍?'), ['areas_2m_compare']) 185 | self.assertEqual(self.check_question('12年港澳台运输总周转量占11年百分之几?'), ['areas_2m_compare']) 186 | self.assertEqual(self.check_question('12年港澳台运输总周转量是11年比例?'), ['areas_2m_compare']) 187 | 188 | self.assertEqual(self.check_question('2011年国内游客周转量比一二年多多少?'), ['areas_2n_compare']) 189 | self.assertEqual(self.check_question('2012年港澳台游客周转量比上一年的少多少?'), ['areas_2n_compare']) 190 | self.assertEqual(self.check_question('2011年港澳台与国内的游客周转量相比12降低多少?'), ['areas_2n_compare']) 191 | self.assertEqual(self.check_question('2011年港澳台的游客周转量同2012相比降低多少?'), ['areas_2n_compare']) 192 | self.assertEqual(self.check_question('2012年的港澳台与去年相比,游客周转量降低多少?'), ['areas_2n_compare']) 193 | self.assertEqual(self.check_question('2013年的港澳台与两年前相比,游客周转量降低多少?'), ['areas_2n_compare']) 194 | # 反例 195 | self.assertEqual(self.check_question('2012年港澳台游客周转量比上一年的货邮周转量少多少?'), []) 196 | 197 | # 指标值变化(多年份) 198 | def test_indexes_trend(self): 199 | self.assertEqual(self.check_question('2011-13年运输总周转量的变化趋势如何?'), ['indexes_trend']) 200 | self.assertEqual(self.check_question('2011-13年运输总周转量情况?'), ['indexes_trend']) 201 | self.assertEqual(self.check_question('2011-13年运输总周转量值分布状况?'), ['indexes_trend']) 202 | self.assertEqual(self.check_question('2013年运输总周转量值与前两年相比变化状况如何?'), ['indexes_trend']) 203 | # 反例 204 | self.assertEqual(self.check_question('2011-12年运输总周转量的变化趋势如何?'), []) 205 | 206 | # 地区指标值变化(多年份) 207 | def test_areas_trend(self): 208 | self.assertEqual(self.check_question('2011-13年国内运输总周转量的变化趋势如何?'), ['areas_trend']) 209 | self.assertEqual(self.check_question('2011-13年国际运输总周转量情况?'), ['areas_trend']) 210 | self.assertEqual(self.check_question('2011-13年港澳台运输总周转量值分布状况?'), ['areas_trend']) 211 | 212 | # 占总指标比的变化 213 | def test_indexes_overall_trend(self): 214 | self.assertEqual(self.check_question('2011-13年运输总周转量占总体的比例的变化形势?'), ['indexes_overall_trend']) 215 | self.assertEqual(self.check_question('2011-13年运输总周转量占父级指标比的情况?'), ['indexes_overall_trend']) 216 | self.assertEqual(self.check_question('2011-13年运输总周转量值占总比的分布状况?'), ['indexes_overall_trend']) 217 | 218 | def test_areas_overall_trend(self): 219 | self.assertEqual(self.check_question('2011-13年国内运输总周转量占总体的比例的变化形势?'), ['areas_overall_trend']) 220 | self.assertEqual(self.check_question('2011-13年国际运输总周转量占父级指标比的情况?'), ['areas_overall_trend']) 221 | self.assertEqual(self.check_question('2011-13年港澳台运输总周转量值占总比的分布状况?'), ['areas_overall_trend']) 222 | 223 | # 指标的变化 224 | def test_indexes_change(self): 225 | self.assertEqual(self.check_question('2011-13年指标变化情况?'), ['indexes_change']) 226 | self.assertEqual(self.check_question('2011-13年指标变化趋势情况?'), ['indexes_change']) 227 | 228 | # 目录的变化 229 | def test_catalogs_change(self): 230 | self.assertEqual(self.check_question('2011-13年目录变化情况?'), ['catalogs_change']) 231 | self.assertEqual(self.check_question('2011-13年规范趋势情况变化?'), ['catalogs_change']) 232 | 233 | # 几个年份中的最值 234 | def test_indexes_and_areas_max(self): 235 | self.assertEqual(self.check_question('2011-13年运输总周转量最大值是?'), ['indexes_max']) 236 | self.assertEqual(self.check_question('2011-13年运输总周转量最小值是哪一年?'), ['indexes_max']) 237 | self.assertEqual(self.check_question('2011-13年国内运输总周转量最大值是?'), ['areas_max']) 238 | 239 | # 何时开始统计此指标 240 | def test_begin_stats(self): 241 | self.assertEqual(self.check_question('哪年统计了航空严重事故征候?'), ['begin_stats']) 242 | self.assertEqual(self.check_question('在哪一年出现了航空公司营业收入数据?'), ['begin_stats']) 243 | self.assertEqual(self.check_question('航空事故征候数据统计出现在哪一年?'), ['begin_stats']) 244 | self.assertEqual(self.check_question('运输周转量数据统计出现在哪一年?'), ['begin_stats']) 245 | 246 | 247 | if __name__ == '__main__': 248 | unittest.main() 249 | -------------------------------------------------------------------------------- /demo/demo2.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "metadata": { 7 | "collapsed": true 8 | }, 9 | "outputs": [ 10 | { 11 | "name": "stdout", 12 | "output_type": "stream", 13 | "text": [ 14 | "欢迎与小航对话,请问有什么可以帮助您的?\n" 15 | ] 16 | } 17 | ], 18 | "source": [ 19 | "import os\n", 20 | "from chatbot import CAChatBot\n", 21 | "\n", 22 | "os.chdir(os.path.join(os.getcwd(), '..'))\n", 23 | "chatbot = CAChatBot()" 24 | ] 25 | }, 26 | { 27 | "cell_type": "markdown", 28 | "source": [ 29 | "### 在这个demo中,你将看到如下几个问题类型的回答和介绍:\n", 30 | "1. index_value\n", 31 | "2. index_overall\n", 32 | "3. index_2_overall\n", 33 | "4. indexes_m_compare\n", 34 | "5. indexes_n_compare\n", 35 | "6. indexes_g_compare\n", 36 | "7. indexes_2m_compare\n", 37 | "8. indexes_2n_compare" 38 | ], 39 | "metadata": { 40 | "collapsed": false 41 | } 42 | }, 43 | { 44 | "cell_type": "markdown", 45 | "source": [ 46 | "## 1. index_value\n", 47 | "回答了某一个年度的某项(可以是一项也可以是多项)指标的值。" 48 | ], 49 | "metadata": { 50 | "collapsed": false 51 | } 52 | }, 53 | { 54 | "cell_type": "code", 55 | "execution_count": 2, 56 | "outputs": [ 57 | { 58 | "data": { 59 | "text/plain": "'货邮周转量为170.29亿吨公里。'" 60 | }, 61 | "execution_count": 2, 62 | "metadata": {}, 63 | "output_type": "execute_result" 64 | } 65 | ], 66 | "source": [ 67 | "chatbot.query('2013年的货邮周转量为?')" 68 | ], 69 | "metadata": { 70 | "collapsed": false, 71 | "pycharm": { 72 | "name": "#%%\n" 73 | } 74 | } 75 | }, 76 | { 77 | "cell_type": "code", 78 | "execution_count": 3, 79 | "outputs": [ 80 | { 81 | "data": { 82 | "text/plain": "'货邮周转量为173.91亿吨公里;旅客周转量为403.53亿吨公里。'" 83 | }, 84 | "execution_count": 3, 85 | "metadata": {}, 86 | "output_type": "execute_result" 87 | } 88 | ], 89 | "source": [ 90 | "chatbot.query('2011年货邮周转量和游客周转量是多少?')\n", 91 | "# 名字近似但不对的,会对其进行模糊查询并匹配最相似的。例 “游客周转量” =》 “旅客周转量”。" 92 | ], 93 | "metadata": { 94 | "collapsed": false, 95 | "pycharm": { 96 | "name": "#%%\n" 97 | } 98 | } 99 | }, 100 | { 101 | "cell_type": "code", 102 | "execution_count": 4, 103 | "outputs": [ 104 | { 105 | "data": { 106 | "text/plain": "'节能减排为2012年,航空公司使用临时航线约有41.3万架次,缩短飞行距离超过1400万公里,节约航油消耗7.6万吨,减少二氧化碳排放约24万吨。\\r\\n\\r\\n2012年完成西安、重庆、长沙、南京、武汉等17个年旅客吞吐量超过500万人次以上机场桥载设备安装立项报告评估和批复工作。“桥载设备替代飞机APU”全国推广专项工作顺利推进,年节能量和减排量将随着机场相关设备的逐步安装使用不断扩大。'" 107 | }, 108 | "execution_count": 4, 109 | "metadata": {}, 110 | "output_type": "execute_result" 111 | } 112 | ], 113 | "source": [ 114 | "chatbot.query('2012年节能减排的值怎样?')" 115 | ], 116 | "metadata": { 117 | "collapsed": false, 118 | "pycharm": { 119 | "name": "#%%\n" 120 | } 121 | } 122 | }, 123 | { 124 | "cell_type": "markdown", 125 | "source": [ 126 | "## 2. index_overall\n", 127 | "回答某一个年度的某项(一或多项)指标占其总指标的百分比和倍数。" 128 | ], 129 | "metadata": { 130 | "collapsed": false, 131 | "pycharm": { 132 | "name": "#%% md\n" 133 | } 134 | } 135 | }, 136 | { 137 | "cell_type": "code", 138 | "execution_count": 5, 139 | "outputs": [ 140 | { 141 | "data": { 142 | "text/plain": "'旅客周转量为501.43亿吨公里,其占总体(运输总周转量)的74.65%,总体(运输总周转量)是其的1.34倍。'" 143 | }, 144 | "execution_count": 5, 145 | "metadata": {}, 146 | "output_type": "execute_result" 147 | } 148 | ], 149 | "source": [ 150 | "chatbot.query('2013年旅客周转量占其总体百分之多少?')" 151 | ], 152 | "metadata": { 153 | "collapsed": false, 154 | "pycharm": { 155 | "name": "#%%\n" 156 | } 157 | } 158 | }, 159 | { 160 | "cell_type": "code", 161 | "execution_count": 6, 162 | "outputs": [ 163 | { 164 | "data": { 165 | "text/plain": "'货邮周转量为170.29亿吨公里,其占总体(运输总周转量)的25.35%,总体(运输总周转量)是其的3.945倍;无全行业取得驾驶执照飞行员的父级数据记录,无法比较。'" 166 | }, 167 | "execution_count": 6, 168 | "metadata": {}, 169 | "output_type": "execute_result" 170 | } 171 | ], 172 | "source": [ 173 | "chatbot.query('2013年货邮周转量和全行业取得驾驶执照飞行员占总体多少?')" 174 | ], 175 | "metadata": { 176 | "collapsed": false, 177 | "pycharm": { 178 | "name": "#%%\n" 179 | } 180 | } 181 | }, 182 | { 183 | "cell_type": "markdown", 184 | "source": [ 185 | "## 3. index_2_overall\n", 186 | "回答两个年度的一项指标占比总指标的变化(主要指占比是增加还是减少)。" 187 | ], 188 | "metadata": { 189 | "collapsed": false, 190 | "pycharm": { 191 | "name": "#%% md\n" 192 | } 193 | } 194 | }, 195 | { 196 | "cell_type": "code", 197 | "execution_count": 7, 198 | "outputs": [ 199 | { 200 | "data": { 201 | "text/plain": "'2012年旅客周转量为446.43亿吨公里,其总体指标(运输总周转量)的为610.32亿吨公里,约占总体的73.15%;2011年旅客周转量为403.53亿吨公里,其总体指标(运输总周转量)的为577.44亿吨公里,约占总体的69.88%;前者相比后者提高3.27%。'" 202 | }, 203 | "execution_count": 7, 204 | "metadata": {}, 205 | "output_type": "execute_result" 206 | } 207 | ], 208 | "source": [ 209 | "chatbot.query('2012年游客周转量占总体的百分比比去年变化多少?')" 210 | ], 211 | "metadata": { 212 | "collapsed": false, 213 | "pycharm": { 214 | "name": "#%%\n" 215 | } 216 | } 217 | }, 218 | { 219 | "cell_type": "code", 220 | "execution_count": 8, 221 | "outputs": [ 222 | { 223 | "data": { 224 | "text/plain": "'2012年货邮周转量为163.89亿吨公里,其总体指标(运输总周转量)的为610.32亿吨公里,约占总体的26.85%;2011年货邮周转量为173.91亿吨公里,其总体指标(运输总周转量)的为577.44亿吨公里,约占总体的30.12%;前者相比后者降低3.27%。'" 225 | }, 226 | "execution_count": 8, 227 | "metadata": {}, 228 | "output_type": "execute_result" 229 | } 230 | ], 231 | "source": [ 232 | "chatbot.query('2012年游客周转量和货邮周转量占总体的百分比比去年变化多少?')" 233 | ], 234 | "metadata": { 235 | "collapsed": false, 236 | "pycharm": { 237 | "name": "#%%\n" 238 | } 239 | } 240 | }, 241 | { 242 | "cell_type": "code", 243 | "execution_count": 9, 244 | "outputs": [ 245 | { 246 | "data": { 247 | "text/plain": "'无2013、2011这几年运输总周转量的父级数据记录,无法比较。'" 248 | }, 249 | "execution_count": 9, 250 | "metadata": {}, 251 | "output_type": "execute_result" 252 | } 253 | ], 254 | "source": [ 255 | "chatbot.query('13年的运输总周转量占父级的倍数比11年降低多少?')" 256 | ], 257 | "metadata": { 258 | "collapsed": false, 259 | "pycharm": { 260 | "name": "#%%\n" 261 | } 262 | } 263 | }, 264 | { 265 | "cell_type": "markdown", 266 | "source": [ 267 | "## 4. indexes_m_compare\n", 268 | "回答某一个年度中某一项指标与另一项指标的倍数关系,只有单位相同的指标才可以比较。" 269 | ], 270 | "metadata": { 271 | "collapsed": false 272 | } 273 | }, 274 | { 275 | "cell_type": "code", 276 | "execution_count": 10, 277 | "outputs": [ 278 | { 279 | "data": { 280 | "text/plain": "'旅客周转量为403.53亿吨公里,货邮周转量为173.91亿吨公里,前者是后者的2.32倍,后者是前者的0.431倍。'" 281 | }, 282 | "execution_count": 10, 283 | "metadata": {}, 284 | "output_type": "execute_result" 285 | } 286 | ], 287 | "source": [ 288 | "chatbot.query('2011年旅客周转量是货邮周转量的几倍?')" 289 | ], 290 | "metadata": { 291 | "collapsed": false, 292 | "pycharm": { 293 | "name": "#%%\n" 294 | } 295 | } 296 | }, 297 | { 298 | "cell_type": "code", 299 | "execution_count": 11, 300 | "outputs": [ 301 | { 302 | "data": { 303 | "text/plain": "'旅客周转量的单位(亿吨公里)与新增机场的单位(无)不同,无法比较。'" 304 | }, 305 | "execution_count": 11, 306 | "metadata": {}, 307 | "output_type": "execute_result" 308 | } 309 | ], 310 | "source": [ 311 | "chatbot.query('11年旅客周转量是新增机场数量的几倍?')" 312 | ], 313 | "metadata": { 314 | "collapsed": false, 315 | "pycharm": { 316 | "name": "#%%\n" 317 | } 318 | } 319 | }, 320 | { 321 | "cell_type": "markdown", 322 | "source": [ 323 | "## 5. indexes_n_compare\n", 324 | "回答某一个年度中某一项指标与另一项指标的比较关系(多少关系),同样也只有单位相同才可比较。" 325 | ], 326 | "metadata": { 327 | "collapsed": false, 328 | "pycharm": { 329 | "name": "#%% md\n" 330 | } 331 | } 332 | }, 333 | { 334 | "cell_type": "code", 335 | "execution_count": 12, 336 | "outputs": [ 337 | { 338 | "data": { 339 | "text/plain": "'旅客周转量为501.43亿吨公里,货邮周转量为170.29亿吨公里,前者比后者多331.14亿吨公里。'" 340 | }, 341 | "execution_count": 12, 342 | "metadata": {}, 343 | "output_type": "execute_result" 344 | } 345 | ], 346 | "source": [ 347 | "chatbot.query('13年旅客周转量比货邮周转量多多少?')" 348 | ], 349 | "metadata": { 350 | "collapsed": false, 351 | "pycharm": { 352 | "name": "#%%\n" 353 | } 354 | } 355 | }, 356 | { 357 | "cell_type": "code", 358 | "execution_count": 13, 359 | "outputs": [ 360 | { 361 | "data": { 362 | "text/plain": "'旅客周转量的单位(亿吨公里)与货邮运输量的单位(万吨)不同,无法比较。'" 363 | }, 364 | "execution_count": 13, 365 | "metadata": {}, 366 | "output_type": "execute_result" 367 | } 368 | ], 369 | "source": [ 370 | "chatbot.query('12年旅客周转量比货邮运输量少多少?')" 371 | ], 372 | "metadata": { 373 | "collapsed": false, 374 | "pycharm": { 375 | "name": "#%%\n" 376 | } 377 | } 378 | }, 379 | { 380 | "cell_type": "code", 381 | "execution_count": 14, 382 | "outputs": [ 383 | { 384 | "data": { 385 | "text/plain": "'旅客周转量的单位(亿吨公里)与新增机场的单位(无)不同,无法比较。'" 386 | }, 387 | "execution_count": 14, 388 | "metadata": {}, 389 | "output_type": "execute_result" 390 | } 391 | ], 392 | "source": [ 393 | "chatbot.query('11年旅客周转量比新增机场数量多多少?')" 394 | ], 395 | "metadata": { 396 | "collapsed": false, 397 | "pycharm": { 398 | "name": "#%%\n" 399 | } 400 | } 401 | }, 402 | { 403 | "cell_type": "markdown", 404 | "source": [ 405 | "## 6. indexes_g_compare\n", 406 | "回答某一个年度中某项(一或多项)指标的同比变化(同比只能是比去年的数据)。" 407 | ], 408 | "metadata": { 409 | "collapsed": false 410 | } 411 | }, 412 | { 413 | "cell_type": "code", 414 | "execution_count": 15, 415 | "outputs": [ 416 | { 417 | "data": { 418 | "text/plain": "'2013年的旅客运输量为35397.0万人次,其去年的为31936.0万人次,同比增长10.84%。'" 419 | }, 420 | "execution_count": 15, 421 | "metadata": {}, 422 | "output_type": "execute_result" 423 | } 424 | ], 425 | "source": [ 426 | "chatbot.query('2013年旅客运输量同比上升多少?')" 427 | ], 428 | "metadata": { 429 | "collapsed": false, 430 | "pycharm": { 431 | "name": "#%%\n" 432 | } 433 | } 434 | }, 435 | { 436 | "cell_type": "code", 437 | "execution_count": 16, 438 | "outputs": [ 439 | { 440 | "data": { 441 | "text/plain": "'2012年的货邮周转量为163.89亿吨公里,其去年的为173.91亿吨公里,同比降低5.76%;2012年的旅客运输量为31936.0万人次,其去年的为29317.0万人次,同比增长8.93%。'" 442 | }, 443 | "execution_count": 16, 444 | "metadata": {}, 445 | "output_type": "execute_result" 446 | } 447 | ], 448 | "source": [ 449 | "chatbot.query('12年游客运输量和货邮周转量同比变化多少?')" 450 | ], 451 | "metadata": { 452 | "collapsed": false, 453 | "pycharm": { 454 | "name": "#%%\n" 455 | } 456 | } 457 | }, 458 | { 459 | "cell_type": "code", 460 | "execution_count": 17, 461 | "outputs": [ 462 | { 463 | "data": { 464 | "text/plain": "'年报中并未记录“2010”年的数据!'" 465 | }, 466 | "execution_count": 17, 467 | "metadata": {}, 468 | "output_type": "execute_result" 469 | } 470 | ], 471 | "source": [ 472 | "chatbot.query('11年旅客周转量同比增长?')" 473 | ], 474 | "metadata": { 475 | "collapsed": false, 476 | "pycharm": { 477 | "name": "#%%\n" 478 | } 479 | } 480 | }, 481 | { 482 | "cell_type": "markdown", 483 | "source": [ 484 | "## 7. indexes_2m_compare\n", 485 | "回答某两个年度的某一项指标之间的倍数关系,非数值类型无法比较。" 486 | ], 487 | "metadata": { 488 | "collapsed": false 489 | } 490 | }, 491 | { 492 | "cell_type": "code", 493 | "execution_count": 18, 494 | "outputs": [ 495 | { 496 | "data": { 497 | "text/plain": "'2013年的旅客周转量(501.43亿吨公里)是2011年的(403.53亿吨公里)1.243倍。'" 498 | }, 499 | "execution_count": 18, 500 | "metadata": {}, 501 | "output_type": "execute_result" 502 | } 503 | ], 504 | "source": [ 505 | "chatbot.query('13年游客周转量是11年的几倍?')" 506 | ], 507 | "metadata": { 508 | "collapsed": false, 509 | "pycharm": { 510 | "name": "#%%\n" 511 | } 512 | } 513 | }, 514 | { 515 | "cell_type": "code", 516 | "execution_count": 19, 517 | "outputs": [ 518 | { 519 | "data": { 520 | "text/plain": "'2013年的运输总周转量(671.72亿吨公里)是2011年的(577.44亿吨公里)1.163倍。'" 521 | }, 522 | "execution_count": 19, 523 | "metadata": {}, 524 | "output_type": "execute_result" 525 | } 526 | ], 527 | "source": [ 528 | "chatbot.query('13年游客周转量和运输总周转量是11年的几倍?')" 529 | ], 530 | "metadata": { 531 | "collapsed": false, 532 | "pycharm": { 533 | "name": "#%%\n" 534 | } 535 | } 536 | }, 537 | { 538 | "cell_type": "code", 539 | "execution_count": 20, 540 | "outputs": [ 541 | { 542 | "data": { 543 | "text/plain": "'新增机场的记录为无效的值类型,无法比较。'" 544 | }, 545 | "execution_count": 20, 546 | "metadata": {}, 547 | "output_type": "execute_result" 548 | } 549 | ], 550 | "source": [ 551 | "chatbot.query('12年新增机场是11年的几倍?')" 552 | ], 553 | "metadata": { 554 | "collapsed": false, 555 | "pycharm": { 556 | "name": "#%%\n" 557 | } 558 | } 559 | }, 560 | { 561 | "cell_type": "markdown", 562 | "source": [ 563 | "## 8. indexes_2n_compare\n", 564 | "回答某两个年度的某项指标之间的比较关系(多少关系),非数值类型无法比较" 565 | ], 566 | "metadata": { 567 | "collapsed": false 568 | } 569 | }, 570 | { 571 | "cell_type": "code", 572 | "execution_count": 21, 573 | "outputs": [ 574 | { 575 | "data": { 576 | "text/plain": "'2012年的货邮运输量(545.0万吨)比2013年的(561.0万吨)减少16.0万吨。'" 577 | }, 578 | "execution_count": 21, 579 | "metadata": {}, 580 | "output_type": "execute_result" 581 | } 582 | ], 583 | "source": [ 584 | "chatbot.query('12年比13年货邮运输量增加了多少?')" 585 | ], 586 | "metadata": { 587 | "collapsed": false, 588 | "pycharm": { 589 | "name": "#%%\n" 590 | } 591 | } 592 | }, 593 | { 594 | "cell_type": "code", 595 | "execution_count": 22, 596 | "outputs": [ 597 | { 598 | "data": { 599 | "text/plain": "'2013年的货邮周转量(170.29亿吨公里)比2012年的(163.89亿吨公里)增加6.4亿吨公里。'" 600 | }, 601 | "execution_count": 22, 602 | "metadata": {}, 603 | "output_type": "execute_result" 604 | } 605 | ], 606 | "source": [ 607 | "chatbot.query('13年同去年相比,货邮周转量变化了多少?')" 608 | ], 609 | "metadata": { 610 | "collapsed": false, 611 | "pycharm": { 612 | "name": "#%%\n" 613 | } 614 | } 615 | }, 616 | { 617 | "cell_type": "code", 618 | "execution_count": 23, 619 | "outputs": [ 620 | { 621 | "data": { 622 | "text/plain": "'节能减排的记录为无效的值类型,无法比较。'" 623 | }, 624 | "execution_count": 23, 625 | "metadata": {}, 626 | "output_type": "execute_result" 627 | } 628 | ], 629 | "source": [ 630 | "chatbot.query('2012年节能减排比去年变化了多少?')\n" 631 | ], 632 | "metadata": { 633 | "collapsed": false, 634 | "pycharm": { 635 | "name": "#%%\n" 636 | } 637 | } 638 | } 639 | ], 640 | "metadata": { 641 | "kernelspec": { 642 | "display_name": "Python 3", 643 | "language": "python", 644 | "name": "python3" 645 | }, 646 | "language_info": { 647 | "codemirror_mode": { 648 | "name": "ipython", 649 | "version": 2 650 | }, 651 | "file_extension": ".py", 652 | "mimetype": "text/x-python", 653 | "name": "python", 654 | "nbconvert_exporter": "python", 655 | "pygments_lexer": "ipython2", 656 | "version": "2.7.6" 657 | } 658 | }, 659 | "nbformat": 4, 660 | "nbformat_minor": 0 661 | } -------------------------------------------------------------------------------- /test/answer_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import os 3 | 4 | from chatbot import CAChatBot 5 | 6 | os.chdir(os.path.join(os.getcwd(), '..')) 7 | 8 | 9 | class AnswerTest(unittest.TestCase): 10 | 11 | bot = CAChatBot() 12 | 13 | def search(self, question: str): 14 | ans = self.bot.query(question) 15 | return ans 16 | 17 | def test_year_status(self): 18 | self.assertEqual(self.search('2011年总体情况怎样?'), 19 | '2011年,全年航空安全形势稳定,旅客运输和通用航空保持较快增长,运行质量和经济效益得到提升,基础设施建设取得新成绩,结构调整和深化改革迈出新步伐,党的建设和行业文化建设得到加强。') 20 | self.assertEqual(self.search('2011年发展形势怎样?'), 21 | '2011年,全年航空安全形势稳定,旅客运输和通用航空保持较快增长,运行质量和经济效益得到提升,基础设施建设取得新成绩,结构调整和深化改革迈出新步伐,党的建设和行业文化建设得到加强。') 22 | self.assertEqual(self.search('2011年发展如何?'), 23 | '2011年,全年航空安全形势稳定,旅客运输和通用航空保持较快增长,运行质量和经济效益得到提升,基础设施建设取得新成绩,结构调整和深化改革迈出新步伐,党的建设和行业文化建设得到加强。') 24 | self.assertEqual(self.search('15年形势怎样?'), 25 | '2015年,全行业深入贯彻党的十八大、十八届五中全会和中央经济工作会议精神,认真落实中央领导对民航发展作出的重要指示, 深入推进实施民航强国战略, 努力践行“发展为了人民”的理念, 坚持“飞行安全,廉政安全,真情服务”三个底线, 稳中求进, 深化改革,各项工作取得较大成绩,民航业在经济社会发展中战略作用更加显现。') 26 | 27 | def test_catalog_status(self): 28 | self.assertEqual(self.search('2011年运输航空总体情况怎样?'), 29 | '2011年,民航运输发展稳中向好,实现了“十二五”时期的良好开局。') 30 | self.assertEqual(self.search('2011年航空安全与服务质量发展形势怎样?'), 31 | '2011年,民航绝大多数运行单位安全形势平稳。全行业没有发生空防安全事故、重大航空地面事故和特大航空器维修事故。') 32 | self.assertEqual(self.search('2011年教育与科技发展如何?'), 33 | '并没有关于2011年教育与科技的描述。') 34 | self.assertEqual(self.search('2011固定资产投资和航空安全与服务质量发展形势怎样?'), 35 | '并没有关于2011年固定资产投资的描述;2011年,民航绝大多数运行单位安全形势平稳。全行业没有发生空防安全事故、重大航空地面事故和特大航空器维修事故。') 36 | 37 | def test_exist_catalog(self): 38 | # 回答中的词语出现位置不固定 39 | pass 40 | 41 | def test_index_value(self): 42 | self.assertEqual(self.search('2011年的货邮周转量和游客周转量是多少?'), 43 | '货邮周转量为173.91亿吨公里;旅客周转量为403.53亿吨公里。') 44 | self.assertEqual(self.search('2011年的货邮周转量的值是?'), 45 | '货邮周转量为173.91亿吨公里。') 46 | self.assertEqual(self.search('2011年的货邮周转量为?'), 47 | '货邮周转量为173.91亿吨公里。') 48 | self.assertEqual(self.search('2011年的货邮周转量是'), 49 | '货邮周转量为173.91亿吨公里。') 50 | 51 | def test_area_value(self): 52 | self.assertEqual(self.search('11年国内的运输总周转量为?'), 53 | '国内航线运输总周转量为380.61亿吨公里。') 54 | self.assertEqual(self.search('11年国内的运总周转量为?'), 55 | '国内航线运输总周转量为380.61亿吨公里。') 56 | self.assertEqual(self.search('11年国内和国际的旅客运输量为'), 57 | '国内航线旅客运输量为27199.0万人次;国际航线旅客运输量为2118.0万人次。') 58 | self.assertEqual(self.search('11年国内和国际的游客运输量为'), 59 | '国内航线旅客运输量为27199.0万人次;国际航线旅客运输量为2118.0万人次。') 60 | self.assertEqual(self.search('11年国际方面运输总周转量是多少?'), 61 | '国际航线运输总周转量为196.84亿吨公里。') 62 | self.assertEqual(self.search('11年国际方面运输总周转量和货邮周转量是多少?'), 63 | '国际航线运输总周转量为196.84亿吨公里;国际货邮周转量无数据记录。') 64 | self.assertEqual(self.search('11年国内和国际方面运输总周转量和货邮周转量是多少?'), 65 | '国内航线运输总周转量为380.61亿吨公里;国内货邮周转量无数据记录;国际航线运输总周转量为196.84亿吨公里;国际货邮周转量无数据记录。') 66 | 67 | def test_index_area_overall(self): 68 | # index 69 | self.assertEqual(self.search('2011年的游客周转量和运输总周转量占总体多少?'), 70 | '无运输总周转量的父级指标数据记录,无法比较;旅客周转量为403.53亿吨公里,其父级指标运输总周转量为577.44亿吨公里,前者占后者的69.88%,后者是前者的1.431倍。') 71 | self.assertEqual(self.search('2011年的游客周转量占父指标多少份额?'), 72 | '旅客周转量为403.53亿吨公里,其父级指标运输总周转量为577.44亿吨公里,前者占后者的69.88%,后者是前者的1.431倍。') 73 | self.assertEqual(self.search('2017年游客周转量是其总体的几倍?'), 74 | '旅客周转量为9513.04亿人公里,其父级指标运输总周转量为1083.08亿吨公里,两者单位不同,无法比较。') 75 | # area 76 | self.assertEqual(self.search('11年国际和国内的运输总周转量占总值的多少?'), 77 | '无国际运输总周转量的父级数据记录,无法比较;无国内运输总周转量的父级数据记录,无法比较。') 78 | self.assertEqual(self.search('11年我国西部地区的运输总周转量和货邮吞吐量是全体的多少倍?'), 79 | '无西部地区运输总周转量的数据记录,无法比较。') 80 | self.assertEqual(self.search('11年港澳台和国际的运输总周转量和旅客运输量是父级的多少倍?'), 81 | '港澳台航线运输总周转量为12.64亿吨公里,其父级国内航线运输总周转量为380.61亿吨公里,前者占后者的3.32%,后者是前者的30.112倍;无港澳台旅客运输量的父级数据记录,无法比较。') 82 | self.assertEqual(self.search('2017年港澳台旅客周转量占其总体地区的多少?'), 83 | '港澳台航线旅客周转量为148.25亿人公里,其父级国内航线旅客周转量为7036.53亿人公里,前者占后者的2.11%,后者是前者的47.464倍。') 84 | 85 | def test_index_area_2_overall(self): 86 | # index 87 | self.assertEqual(self.search('2012年游客周转量占总体的百分比比去年变化多少?'), 88 | '2012年旅客周转量为446.43亿吨公里,其总体运输总周转量为610.32亿吨公里,约占总体的73.15%;2011年旅客周转量为403.53亿吨公里,其总体运输总周转量为577.44亿吨公里,约占总体的69.88%;前者相比后者提高3.27%。') 89 | self.assertEqual(self.search('2017年游客周转量占总体的百分比,相比11年变化多少?'), 90 | '2017年旅客周转量为9513.04亿人公里,其总体运输总周转量为1083.08亿吨公里,两者单位不同,无法比较;2011年旅客周转量为403.53亿吨公里,其总体运输总周转量为577.44亿吨公里,约占总体的69.88%。') 91 | self.assertEqual(self.search('2012年相比11年,游客周转量占总体的百分比变化多少?'), 92 | '2012年旅客周转量为446.43亿吨公里,其总体运输总周转量为610.32亿吨公里,约占总体的73.15%;2011年旅客周转量为403.53亿吨公里,其总体运输总周转量为577.44亿吨公里,约占总体的69.88%;前者相比后者提高3.27%。') 93 | self.assertEqual(self.search('2012年的运输总周转量占父级的倍数比11年降低多少?'), 94 | '无2012、2011这几年运输总周转量的父级数据记录,无法比较。') 95 | # area 96 | self.assertEqual(self.search('2012年港澳台的游客运输量占总体的百分比比去年变化多少?'), 97 | '2012年港澳台航线旅客运输量为834.0万人次,其总体国内航线为29600.0万人次,约占总体的2.82%;2011年港澳台航线旅客运输量为760.0万人次,其总体国内航线为27199.0万人次,约占总体的2.79%;前者相比后者提高0.03%。') 98 | self.assertEqual(self.search('2012年中部地区旅客吞吐量占总体的百分比,相比11年变化多少?'), 99 | '无2012、2011这几年中部地区旅客吞吐量的父级数据记录,无法比较。') 100 | self.assertEqual(self.search('2012年相比11年,港澳台游客运输量占总体的百分比变化多少?'), 101 | '2012年港澳台航线旅客运输量为834.0万人次,其总体国内航线为29600.0万人次,约占总体的2.82%;2011年港澳台航线旅客运输量为760.0万人次,其总体国内航线为27199.0万人次,约占总体的2.79%;前者相比后者提高0.03%。') 102 | self.assertEqual(self.search('2012年的国内游客周转量占总计比例比去年增加多少?'), 103 | '无2012、2011这几年国内旅客周转量的数据记录,无法比较。') 104 | 105 | def test_indexes_m_n_compare(self): 106 | # m 107 | self.assertEqual(self.search('2011年游客周转量是货邮周转量的几倍?'), 108 | '货邮周转量为173.91亿吨公里,旅客周转量为403.53亿吨公里,前者是后者的0.431倍,后者是前者的2.32倍。') 109 | self.assertEqual(self.search('11年游客周转量是旅客运输量的几倍?'), 110 | '旅客运输量的单位(万人次)与旅客周转量的单位(亿吨公里)不同,无法比较。') 111 | self.assertEqual(self.search('11年游客周转量是新增机场的几倍?'), 112 | '新增机场的单位(无)与旅客周转量的单位(亿吨公里)不同,无法比较。') 113 | self.assertEqual(self.search('2011年游客周转量是货邮周转量的百分之几?'), 114 | '货邮周转量为173.91亿吨公里,旅客周转量为403.53亿吨公里,前者是后者的0.431倍,后者是前者的2.32倍。') 115 | # n 116 | self.assertEqual(self.search('11年游客周转量比货邮周转量多多少?'), 117 | '货邮周转量为173.91亿吨公里,旅客周转量为403.53亿吨公里,前者比后者少229.62亿吨公里。') 118 | self.assertEqual(self.search('11年游客周转量比游客运输量大?'), 119 | '旅客周转量的单位(亿吨公里)与旅客运输量的单位(万人次)不同,无法比较。') 120 | self.assertEqual(self.search('11年游客周转量比货邮周转量少多少?'), 121 | '货邮周转量为173.91亿吨公里,旅客周转量为403.53亿吨公里,前者比后者少229.62亿吨公里。') 122 | 123 | def test_indexes_2m_2n_compare(self): 124 | # 2m 125 | self.assertEqual(self.search('2011年游客周转量是12年的百分之几?'), 126 | '2011年的旅客周转量为403.53亿吨公里,2012年的旅客周转量为446.43亿吨公里,前者是后者的0.904倍。') 127 | self.assertEqual(self.search('2011年的是12年货邮周转量的百分之几?'), 128 | '2011年的货邮周转量为173.91亿吨公里,2012年的货邮周转量为163.89亿吨公里,前者是后者的1.061倍。') 129 | self.assertEqual(self.search('2011年旅客运输量占12的百分之?'), 130 | '2011年的旅客运输量为29317.0万人次,2012年的旅客运输量为31936.0万人次,前者是后者的0.918倍。') 131 | self.assertEqual(self.search('2011年游客周转量是13年的几倍?'), 132 | '2011年的旅客周转量为403.53亿吨公里,2013年的旅客周转量为501.43亿吨公里,前者是后者的0.805倍。') 133 | self.assertEqual(self.search('2011年游客周转量和运输总周转量为12年的多少倍?'), 134 | '2011年的运输总周转量为577.44亿吨公里,2012年的运输总周转量为610.32亿吨公里,前者是后者的0.946倍。') 135 | # 2n 136 | self.assertEqual(self.search('2011年游客周转量比12年降低了?'), 137 | '2011年的旅客周转量为403.53亿吨公里,2012年的旅客周转量为446.43亿吨公里,前者比后者减少42.9亿吨公里。') 138 | self.assertEqual(self.search('2012年节能减排比去年多了多少?'), 139 | '节能减排的记录为无效的值类型,无法比较。') 140 | self.assertEqual(self.search('13年的货邮周转量同2年前相比变化了多少?'), 141 | '2013年的货邮周转量为170.29亿吨公里,2011年的货邮周转量为173.91亿吨公里,前者比后者减少3.62亿吨公里。') 142 | self.assertEqual(self.search('12年同去年相比,货邮周转量变化了多少?'), 143 | '2012年的货邮周转量为163.89亿吨公里,2011年的货邮周转量为173.91亿吨公里,前者比后者减少10.02亿吨公里。') 144 | 145 | def test_indexes_g_compare(self): 146 | self.assertEqual(self.search('2012年游客周转量同比增长多少?'), 147 | '2012年的旅客周转量为446.43亿吨公里,其去年的为403.53亿吨公里,同比增长10.63%。') 148 | self.assertEqual(self.search('2012年重大运输任务同比增长多少?'), 149 | '2012年重大运输任务的记录非数值类型,无法计算。') 150 | self.assertEqual(self.search('2012年游客周转量同比下降百分之几?'), 151 | '2012年的旅客周转量为446.43亿吨公里,其去年的为403.53亿吨公里,同比增长10.63%。') 152 | self.assertEqual(self.search('2012年游客周转量和货邮周转量同比下降百分之几?'), 153 | '2012年的货邮周转量为163.89亿吨公里,其去年的为173.91亿吨公里,同比降低5.76%;2012年的旅客周转量为446.43亿吨公里,其去年的为403.53亿吨公里,同比增长10.63%。') 154 | 155 | def test_areas_m_n_compare(self): 156 | # m 157 | self.assertEqual(self.search('11年港澳台货邮周转量占国内的百分之几?'), 158 | '无港澳台货邮周转量数据记录,无法比较。') 159 | self.assertEqual(self.search('11年国内的运输总周转量是港澳台的几倍?'), 160 | '国内航线运输总周转量为380.61亿吨公里,港澳台航线运输总周转量为12.64亿吨公里,前者是后者的30.112倍,后者是前者的0.033倍。') 161 | self.assertEqual(self.search('11年国际运输总周转量是国内的多少倍?'), 162 | '国际航线运输总周转量为196.84亿吨公里,国内航线运输总周转量为380.61亿吨公里,前者是后者的0.517倍,后者是前者的1.934倍。') 163 | self.assertEqual(self.search('11年港澳台运输总周转量和游客周转量是国际的多少倍?'), 164 | '港澳台航线运输总周转量为12.64亿吨公里,国际航线运输总周转量为196.84亿吨公里,前者是后者的0.064倍,后者是前者的15.573倍;无港澳台旅客周转量数据记录,无法比较。') 165 | # n 166 | self.assertEqual(self.search('2011年国内货邮运输量比国际多多少?'), 167 | '国内航线货邮运输量为379.4万吨,国际航线货邮运输量为178.1万吨,前者比后者多201.3万吨。') 168 | self.assertEqual(self.search('2011年港澳台游客运输量比国内的少多少?'), 169 | '港澳台航线旅客运输量为760.0万人次,国内航线旅客运输量为27199.0万人次,前者比后者少26439.0万人次。') 170 | self.assertEqual(self.search('2011年港澳台与国内的相比游客周转量降低多少?'), 171 | '无港澳台旅客周转量数据记录,无法比较。') 172 | self.assertEqual(self.search('11年港澳台货邮周转量和游客周转量比国际的多多少?'), 173 | '无港澳台货邮周转量数据记录,无法比较;无港澳台旅客周转量数据记录,无法比较。') 174 | 175 | def test_areas_2m_2n_compare(self): 176 | # 2m 177 | self.assertEqual(self.search('11年港澳台运输总周转量和旅客吞吐量是12年的多少倍?'), 178 | '2011年的港澳台运输总周转量为12.64亿吨公里,2012年的港澳台运输总周转量为13.66亿吨公里,前者是后者的0.925倍。') 179 | self.assertEqual(self.search('12年的是11年港澳台运输总周转量的多少倍?'), 180 | '2012年的港澳台运输总周转量为13.66亿吨公里,2011年的港澳台运输总周转量为12.64亿吨公里,前者是后者的1.081倍。') 181 | self.assertEqual(self.search('12年国内与国际货邮周转量占11年百分之几?'), 182 | '无关于2012年的国内货邮周转量的记录;无关于2012年的国际货邮周转量的记录。') 183 | self.assertEqual(self.search('12年国际运输总周转量是11年比例?'), 184 | '2012年的国际运输总周转量为194.49亿吨公里,2011年的国际运输总周转量为196.84亿吨公里,前者是后者的0.988倍。') 185 | # 2n 186 | self.assertEqual(self.search('2011年国内运输总周转量比一二年多多少?'), 187 | '2011年的国内运输总周转量为380.61亿吨公里,2012年的国内运输总周转量为415.83亿吨公里,前者比后者减少35.22亿吨公里。') 188 | self.assertEqual(self.search('2011年港澳台与国内的游客周转量相比12降低多少?'), 189 | '无关于2011年的港澳台旅客周转量的记录;无关于2011年的国内旅客周转量的记录。') 190 | self.assertEqual(self.search('2011年港澳台的旅客运输量同2012相比降低多少?'), 191 | '2011年的港澳台旅客运输量为760.0万人次,2012年的港澳台旅客运输量为834.0万人次,前者比后者减少74.0万人次。') 192 | self.assertEqual(self.search('2012年的东部地区与去年相比,货邮吞吐量降低多少?'), 193 | '2012年的东部地区货邮吞吐量为926.37万吨,2011年的东部地区货邮吞吐量为905.98万吨,前者比后者增加20.39万吨。') 194 | self.assertEqual(self.search('2012年的北上广与一年前相比,旅客吞吐量降低多少?'), 195 | '2012年的北上广旅客吞吐量为30.7%,2011年的北上广旅客吞吐量为31.9%,前者比后者减少1.2%。') 196 | 197 | def test_areas_g_compare(self): 198 | self.assertEqual(self.search('2012年国内运输总周转量同比增长了?'), 199 | '2012年的国内运输总周转量为415.83亿吨公里,其去年的为380.61亿吨公里,同比增长9.25%。') 200 | self.assertEqual(self.search('2012年中部地区旅客吞吐量同比下降了多少?'), 201 | '2012年的中部地区旅客吞吐量为0.67亿人次,其去年的为0.59亿人次,同比增长13.56%。') 202 | self.assertEqual(self.search('2012年国内游客周转量和货邮运输量同比变化了多少?'), 203 | '2012年的国内货邮运输量为388.5万吨,其去年的为379.4万吨,同比增长2.4%;无2012年关于国内旅客周转量的数据。') 204 | 205 | def test_index_compose(self): 206 | self.assertEqual(self.search('2011年运输总周转量的子集有?'), 207 | '该问题的回答已渲染为图像,详见:results/2011年运输总周转量的子集有?.html。') 208 | self.assertEqual(self.search('2011年航空公司计划航班的组成?'), 209 | '该问题的回答已渲染为图像,详见:results/2011年航空公司计划航班的组成?.html。') 210 | self.assertEqual(self.search('2011年指标停用机场的组成有哪些?'), 211 | '指标“停用机场”没有任何组成;该问题的回答已渲染为图像,详见:results/2011年指标停用机场的组成有哪些?.html。') 212 | self.assertEqual(self.search('2011年全行业累计实现营业收入的子指标组成情况?'), 213 | '该问题的回答已渲染为图像,详见:results/2011年全行业累计实现营业收入的子指标组成情况?.html。') 214 | self.assertEqual(self.search('2011年运输总周转量和货邮周转量的子指标组成情况?'), 215 | '指标“货邮周转量”没有任何组成;该问题的回答已渲染为图像,详见:results/2011年运输总周转量和货邮周转量的子指标组成情况?.html。') 216 | self.assertEqual(self.search('2013年与其他国家或地区签订双边航空运输协定的组成如何?'), 217 | '该问题的回答已渲染为图像,详见:results/2013年与其他国家或地区签订双边航空运输协定的组成如何?.html。') 218 | 219 | def test_index_catalog_change(self): 220 | # catalog 221 | self.assertEqual(self.search('12年比11年多了哪些目录'), 222 | '2011年与2012年相比,未统计1个目录:飞行员数量;2012年与2011年的目录相同。') 223 | self.assertEqual(self.search('12年比去年增加了哪些目录'), 224 | '2011年与2012年相比,未统计1个目录:飞行员数量;2012年与2011年的目录相同。') 225 | # index 回答中的词语出现位置不固定 226 | 227 | def test_indexes_catalogs_change(self): 228 | # index 229 | self.assertEqual(self.search('2011-13年指标变化情况?'), 230 | '该问题的回答已渲染为图像,详见:results/2011-13年指标变化情况?.html。') 231 | self.assertEqual(self.search('2011-17年指标变化趋势情况?'), 232 | '该问题的回答已渲染为图像,详见:results/2011-17年指标变化趋势情况?.html。') 233 | # catalogs 234 | self.assertEqual(self.search('2011-13年目录变化情况?'), 235 | '该问题的回答已渲染为图像,详见:results/2011-13年目录变化情况?.html。') 236 | self.assertEqual(self.search('2011-13年规范趋势情况变化?'), 237 | '该问题的回答已渲染为图像,详见:results/2011-13年规范趋势情况变化?.html。') 238 | 239 | def test_indexes_areas_trend(self): 240 | # index 241 | self.assertEqual(self.search('2011-13年运输总周转量的变化趋势如何?'), 242 | '该问题的回答已渲染为图像,详见:results/2011-13年运输总周转量的变化趋势如何?.html。') 243 | self.assertEqual(self.search('2011-13年节能减排情况?'), 244 | '指标“节能减排”无任何值记录,无法比较。') 245 | self.assertEqual(self.search('2011-13年运输总周转量和旅客周转量情况?'), 246 | '该问题的回答已渲染为图像,详见:results/2011-13年运输总周转量和旅客周转量情况?.html。') 247 | self.assertEqual(self.search('2011-14年运输总周转量值分布状况?'), 248 | '该问题的回答已渲染为图像,详见:results/2011-14年运输总周转量值分布状况?.html。') 249 | self.assertEqual(self.search('2013年运输总周转量值与前两年相比变化状况如何?'), 250 | '该问题的回答已渲染为图像,详见:results/2013年运输总周转量值与前两年相比变化状况如何?.html。') 251 | self.assertEqual(self.search('2011-17年全行业取得驾驶执照飞行员和货邮周转量的变化情况?'), 252 | '该问题的回答已渲染为图像,详见:results/2011-17年全行业取得驾驶执照飞行员和货邮周转量的变化情况?.html。') 253 | # area 254 | self.assertEqual(self.search('2011-13年国内运输总周转量的变化趋势如何?'), 255 | '该问题的回答已渲染为图像,详见:results/2011-13年国内运输总周转量的变化趋势如何?.html。') 256 | self.assertEqual(self.search('2011-13年国际运输总周转量情况?'), 257 | '该问题的回答已渲染为图像,详见:results/2011-13年国际运输总周转量情况?.html。') 258 | self.assertEqual(self.search('2011-13年港澳台运输总周转量值分布状况?'), 259 | '该问题的回答已渲染为图像,详见:results/2011-13年港澳台运输总周转量值分布状况?.html。') 260 | self.assertEqual(self.search('2011-17年我国与地区组织签订双边航空运输协定的变化情况?'), 261 | '该问题的回答已渲染为图像,详见:results/2011-17年我国与地区组织签订双边航空运输协定的变化情况?.html。') 262 | 263 | def test_indexes_areas_overall_trend(self): 264 | # index 265 | self.assertEqual(self.search('2011-13年货邮周转量占总体的比例的变化形势?'), 266 | '该问题的回答已渲染为图像,详见:results/2011-13年货邮周转量占总体的比例的变化形势?.html。') 267 | self.assertEqual(self.search('2011-13年货邮周转量和货邮吞吐量占总体的比例的变化形势?'), 268 | '无关于”货邮吞吐量“的父级记录;该问题的回答已渲染为图像,详见:results/2011-13年货邮周转量和货邮吞吐量占总体的比例的变化形势?.html。') 269 | self.assertEqual(self.search('2011-13年货邮周转量和小型飞机平均日利用率占总体的比例的变化形势?'), 270 | '该问题的回答已渲染为图像,详见:results/2011-13年货邮周转量和小型飞机平均日利用率占总体的比例的变化形势?.html。') 271 | self.assertEqual(self.search('2011-13年停用机场占父级指标比的情况?'), 272 | '无关于”停用机场“的父级记录。') 273 | self.assertEqual(self.search('2011-13年民航直属院校在校研究生占总比的分布状况?'), 274 | '该问题的回答已渲染为图像,详见:results/2011-13年民航直属院校在校研究生占总比的分布状况?.html。') 275 | self.assertEqual(self.search('2011-13年民航直属院校在校研究生和民航直属院校在校中专生占总比的分布状况?'), 276 | '该问题的回答已渲染为图像,详见:results/2011-13年民航直属院校在校研究生和民航直属院校在校中专生占总比的分布状况?.html。') 277 | # area 278 | self.assertEqual(self.search('2011-13年国内运输总周转量占总体的比例的变化形势?'), 279 | '无关于”国内运输总周转量“的父级记录。') 280 | self.assertEqual(self.search('2011-13年国际货邮周转量和旅客周转量占父级指标比的情况?'), 281 | '无关于”国际货邮周转量“的记录。') 282 | self.assertEqual(self.search('2011-13年港澳台运输总周转量值占总比的分布状况?'), 283 | '该问题的回答已渲染为图像,详见:results/2011-13年港澳台运输总周转量值占总比的分布状况?.html。') 284 | 285 | def test_indexes_and_areas_max(self): 286 | self.assertEqual(self.search('2011-13年运输总周转量最大值是?'), 287 | '该问题的回答已渲染为图像,详见:results/2011-13年运输总周转量最大值是?.html。') 288 | self.assertEqual(self.search('2011-13年运输总周转量最小值是哪一年?'), 289 | '该问题的回答已渲染为图像,详见:results/2011-13年运输总周转量最小值是哪一年?.html。') 290 | self.assertEqual(self.search('2011-13年国内运输总周转量最大值是?'), 291 | '该问题的回答已渲染为图像,详见:results/2011-13年国内运输总周转量最大值是?.html。') 292 | 293 | def test_begin_stats(self): 294 | self.assertEqual(self.search('哪年统计了航空严重事故征候?'), 295 | '指标“严重事故征候”最早于2011年开始统计;指标“事故征候”最早于2011年开始统计。') 296 | self.assertEqual(self.search('在哪一年出现了航空公司营业收入数据?'), 297 | '指标“航空公司实现营业收入”最早于2011年开始统计。') 298 | self.assertEqual(self.search('航空事故征候数据统计出现在哪一年?'), 299 | '指标“事故征候”最早于2011年开始统计。') 300 | self.assertEqual(self.search('运输周转量数据统计出现在哪一年?'), 301 | '指标“运输总周转量”最早于2011年开始统计。') 302 | 303 | 304 | if __name__ == '__main__': 305 | unittest.main() 306 | -------------------------------------------------------------------------------- /answer_search.py: -------------------------------------------------------------------------------- 1 | # 语句查询及组织回答 2 | from operator import truediv, sub 3 | 4 | from py2neo import Graph 5 | 6 | from lib.utils import sign, debug 7 | from lib.result import Result 8 | from lib.answer import Answer, AnswerBuilder 9 | from lib.painter import Painter 10 | from lib.formatter import Formatter 11 | from lib.chain import TranslationChain 12 | 13 | from const import URI, USERNAME, PASSWORD, CHART_RENDER_DIR 14 | 15 | 16 | class AnswerSearcher: 17 | 18 | def __init__(self): 19 | self.graph = Graph(URI, auth=(USERNAME, PASSWORD)) 20 | self.painter = Painter() 21 | 22 | def search(self, result: Result) -> [Answer]: 23 | debug('||QUESTION ORIGINAL||', result.raw_question) 24 | debug('||QUESTION FILTERED||', result.filtered_question) 25 | answers = [] 26 | for qt, chain in result.sqls.items(): 27 | answer = self.organize(qt, chain, result) 28 | answers.append(answer) 29 | return answers 30 | 31 | def _search_direct(self, sql_gen, offset: int = 0, unpack: bool = False) -> list: 32 | """ 进行直接查询 """ 33 | # 只支持双层列表的嵌套,有第三层列表嵌套时令unpack=True 34 | 35 | def perform_sql(query_sql: str): 36 | rs = self.graph.run(query_sql).data() 37 | if len(rs) > 1: 38 | return [Formatter(r) for r in rs] 39 | elif len(rs) == 1: 40 | return Formatter(rs[0]) 41 | else: 42 | return Formatter(rs) 43 | 44 | results = [] 45 | if isinstance(sql_gen, TranslationChain): 46 | generator = sql_gen.iter(offset, unpack) 47 | else: 48 | generator = sql_gen 49 | for sqls in generator: 50 | debug('||GENERATED SQL||', sqls) 51 | if isinstance(sqls, list): 52 | sub_results = [] 53 | for sql in sqls: 54 | if sql is None: 55 | sub_results.append(Formatter(None)) 56 | continue 57 | sub_results.append(perform_sql(sql)) 58 | results.append(sub_results) 59 | else: 60 | sql = sqls 61 | if sql is None: 62 | results.append(Formatter(None)) 63 | continue 64 | results.append(perform_sql(sql)) 65 | return results 66 | 67 | def _search_direct_then_feed(self, chain: TranslationChain, unpack_key_name: str) -> tuple: 68 | """ 将第一次直接查询的结果作为第二次查询的输入 """ 69 | results_1 = self._search_direct(chain) 70 | pattern_sql = next(chain.iter(1)) 71 | sqls = [] 72 | for items in results_1: 73 | if not items: 74 | sqls.append(None) 75 | continue 76 | for item in items: 77 | sqls.append(pattern_sql.format(item[unpack_key_name])) 78 | results_2 = self._search_direct(sqls) 79 | return results_1, results_2 80 | 81 | def _search_double_direct_then_feed(self, chain: TranslationChain, unpack: bool = False) -> tuple: 82 | """ 第一次直接查询,第二次先执行直接查询后将查询结果投递至最后的查询 """ 83 | results_1 = self._search_direct(chain, unpack=unpack) 84 | temp_res = self._search_direct(chain, 1) 85 | final_sqls = [] 86 | for feed in temp_res: 87 | sqls = [] 88 | for pattern_sql in chain.iter(2, unpack=unpack): 89 | if not feed: 90 | sqls.append(None) 91 | continue 92 | sqls.append(pattern_sql.format(feed.name)) 93 | final_sqls.append(sqls) 94 | results_2 = self._search_direct(final_sqls) 95 | return results_1, results_2, temp_res 96 | 97 | def organize(self, qt: str, chain: TranslationChain, result: Result) -> Answer: 98 | answer = Answer() 99 | builder = AnswerBuilder(answer) 100 | # 年度总体状况 101 | if qt == 'year_status': 102 | self.make_year_status_ans(answer, chain, result) 103 | # 年度目录状况 104 | elif qt == 'catalog_status': 105 | self.make_catalog_status_ans(answer, builder, chain, result) 106 | # 年度目录包含哪些 107 | elif qt == 'exist_catalog': 108 | self.make_exist_catalog_ans(answer, chain, result) 109 | # 指标值 110 | elif qt == 'index_value': 111 | self.make_index_value_ans(answer, builder, chain, result) 112 | # 指标占总比 & 地区指标占总比 113 | elif qt in ('index_overall', 'area_overall'): 114 | self.make_index_or_area_overall_ans(qt, answer, builder, chain, result) 115 | # 指标组成 116 | elif qt == 'index_compose': 117 | self.make_index_compose_ans(answer, builder, chain, result) 118 | # 指标倍数比较(只有两个指标) & 指标数量比较(只有两个指标) 119 | elif qt in ('indexes_m_compare', 'indexes_n_compare'): 120 | self.make_indexes_m_or_n_compare_ans(qt, answer, builder, chain, result) 121 | elif qt in ('indexes_2m_compare', 'indexes_2n_compare', 'areas_2m_compare', 'areas_2n_compare'): 122 | self.make_indexes_or_areas_2m_or_2n_compare_ans(qt, answer, builder, chain, result) 123 | # 指标值同比比较 124 | elif qt == 'indexes_g_compare': 125 | self.make_indexes_g_compare_ans(answer, builder, chain, result) 126 | # 地区指标值 127 | elif qt == 'area_value': 128 | self.make_area_value_ans(answer, builder, chain, result) 129 | # 地区指标占总比的变化 & 指标占总比的变化 130 | elif qt in ('area_2_overall', 'index_2_overall'): 131 | self.make_index_or_area_2_overall_ans(qt, answer, builder, chain, result) 132 | # 地区指标倍数比较(只有两个地区) & 地区指标数量比较(只有两个地区) 133 | elif qt in ('areas_m_compare', 'areas_n_compare'): 134 | self.make_areas_m_or_n_compare_ans(qt, answer, builder, chain, result) 135 | # 地区指标值同比比较 136 | elif qt == 'areas_g_compare': 137 | self.make_areas_g_compare_ans(answer, builder, chain, result) 138 | # 两年目录的变化 & 两年指标的变化 139 | elif qt in ('catalog_change', 'index_change'): 140 | self.make_catalog_or_index_change_ans(qt, answer, builder, chain, result) 141 | # 多年目录的变化 & 多年指标的变化 142 | elif qt in ('catalogs_change', 'indexes_change'): 143 | self.make_catalogs_or_indexes_change_ans(qt, answer, chain, result) 144 | # 指标值变化(多年份) 145 | elif qt in ('indexes_trend', 'areas_trend'): 146 | self.make_indexes_or_areas_trend_ans(qt, answer, builder, chain, result) 147 | # 占总指标比的变化 148 | elif qt in ('indexes_overall_trend', 'areas_overall_trend'): 149 | self.make_indexes_or_areas_overall_trend_ans(qt, answer, builder, chain, result) 150 | # 几个年份中的最值 151 | elif qt in ('indexes_max', 'areas_max'): 152 | self.make_indexes_or_areas_max_ans(qt, answer, builder, chain, result) 153 | # 何时开始统计此指标 154 | elif qt == 'begin_stats': 155 | self.make_begin_stats_ans(answer, builder, chain, result) 156 | 157 | return answer 158 | 159 | def make_year_status_ans(self, answer: Answer, chain: TranslationChain, result: Result): 160 | data = self._search_direct(chain) 161 | answer.add_answer(f'{result["year"][0]}年,{data[0].info}') 162 | 163 | def make_catalog_status_ans(self, answer: Answer, builder: AnswerBuilder, 164 | chain: TranslationChain, result: Result): 165 | data = self._search_direct(chain) 166 | builder.feed_data(data) 167 | for item, name in builder.product_data_with_name( 168 | result['catalog'], 169 | if_is_none=lambda _, na: f'并没有关于{result["year"][0]}年{na.subject()}的描述' 170 | ): 171 | answer.add_answer(item.info) 172 | 173 | def make_exist_catalog_ans(self, answer: Answer, chain: TranslationChain, result: Result): 174 | data = self._search_direct(chain) 175 | if not all(data): 176 | answer.add_answer(f'无{result["year"][0]}年的记录。') 177 | else: 178 | answer.add_answer(f'{result["year"][0]}年目录包括: ' + ','.join([item.name for item in data[0]])) 179 | 180 | def make_index_value_ans(self, answer: Answer, builder: AnswerBuilder, 181 | chain: TranslationChain, result: Result): 182 | data = self._search_direct(chain) 183 | builder.feed_data(data) 184 | for item, name in builder.product_data_with_name( 185 | result['index'], if_is_none=lambda _, na: f'无{na.subject()}数据记录' 186 | ): 187 | answer.add_answer(f'{name.subject()}为{item.val()}') 188 | 189 | def make_index_compose_ans(self, answer: Answer, builder: AnswerBuilder, 190 | chain: TranslationChain, result: Result): 191 | data = self._search_direct(chain) 192 | builder.feed_data(data) 193 | collect = [] 194 | units = [] 195 | sub_titles = [] 196 | # for overall 197 | sqls_overall = [sql for sql in chain.iter(3)] 198 | data_overall = self._search_direct(sqls_overall) 199 | # collect 200 | for total, (item, name) in zip( 201 | data_overall, 202 | builder.product_data_with_name(result['index']) 203 | ): # 为使两可迭代对象同步迭代和collect不用做判空,此处不使用if_is_none参数 204 | if not item: 205 | answer.add_answer(f'指标“{name.name}”没有任何组成') 206 | continue 207 | indexes, areas = [], [] 208 | for n in item: 209 | n.life_check(result['year'][0]) 210 | if n: 211 | if n.label == 'Index': 212 | indexes.append(n.name) 213 | else: 214 | areas.append(n.name) 215 | if len(indexes) == 0 and len(areas) == 0: 216 | answer.add_answer(f'指标“{name.name}”没有任何组成') 217 | continue 218 | # for indexes 219 | sqls1 = [sql.format(i) for sql in chain.iter(1) for i in indexes] 220 | data1 = self._search_direct(sqls1) 221 | # for areas 222 | sqls2 = [sql.format(name.name, a) for sql in chain.iter(2) for a in areas] 223 | data2 = self._search_direct(sqls2) 224 | # make data pairs 225 | final_data = {} 226 | for k, v in zip(indexes + areas, data1 + data2): 227 | if not v: 228 | continue 229 | if v.child_id is None: 230 | continue 231 | try: 232 | final_data.setdefault(v.child_id, []).append((k, float(v.value))) 233 | except ValueError or TypeError: 234 | answer.add_answer(f'{name.name}中{k}的记录非数值类型,无法比较') 235 | return 236 | # make other 237 | for k, v in final_data.items(): 238 | op1 = sum([x[1] for x in v]) 239 | op2 = float(total.value) 240 | if op1 < int(op2): # 舍弃一些误差,避免图中出现极小的部分 241 | final_data[k].append(('其他', round(op2 - op1, 2))) 242 | collect.append(final_data) 243 | units.append(total.unit) 244 | sub_titles.append(f'{name.subject()}为{total.val()},其构成分为:') 245 | # paint 246 | for pie in self.painter.paint_pie(collect, units, 247 | title=result.raw_question, sub_titles=sub_titles): 248 | answer.save_chart(pie) 249 | answer.add_answer(f'该问题的回答已渲染为图像,详见:{CHART_RENDER_DIR}/{result.raw_question}.html') 250 | 251 | def make_indexes_m_or_n_compare_ans(self, qt: str, answer: Answer, builder: AnswerBuilder, 252 | chain: TranslationChain, result: Result): 253 | data = self._search_direct(chain) 254 | builder.feed_data(data) 255 | operator = truediv if qt == 'indexes_m_compare' else sub 256 | for (x, y), (n1, n2) in builder.product_data_with_binary( 257 | result['index'], 258 | if_x_is_none=lambda _1, _2, na: f'无{na[0].subject()}数据记录,无法比较', 259 | if_y_is_none=lambda _1, _2, na: f'无{na[1].subject()}数据记录,无法比较' 260 | ): 261 | # 单位检查 262 | ux, uy = x.unit or '无', y.unit or '无' 263 | if builder.add_if_is_equal_or_not(ux, uy, 264 | no=f'{n1.subject()}的单位({ux})与{n2.subject()}的单位({uy})不同,无法比较'): 265 | answer.begin_sub_answers() 266 | answer.add_sub_answers(f'{n1.subject()}为{x.val()},{n2.subject()}为{y.val()}') 267 | res1 = builder.binary_calculation(x.value, y.value, operator) 268 | if builder.add_if_is_not_none(res1, 269 | no=f'{n1.subject()}或{n2.subject()}非数值类型,无法比较'): 270 | if qt == 'indexes_m_compare': 271 | answer.add_sub_answers(f'前者是后者的{res1}倍') 272 | else: 273 | answer.add_sub_answers(f'前者比后者{sign(res1)}{abs(res1)}{ux}') 274 | if qt == 'indexes_m_compare': 275 | res2 = builder.binary_calculation(y.value, x.value, truediv) 276 | if builder.add_if_is_not_none(res2, 277 | no=f'{n1.subject()}或{n2.subject()}非数值类型,无法比较'): 278 | answer.add_sub_answers(f'后者是前者的{res2}倍') 279 | answer.end_sub_answers() 280 | 281 | def make_indexes_g_compare_ans(self, answer: Answer, builder: AnswerBuilder, 282 | chain: TranslationChain, result: Result): 283 | data = self._search_direct(chain) 284 | builder.feed_data(data) 285 | for item, name in builder.product_data_with_name(result['index']): 286 | x, y = item 287 | if builder.binary_decision( 288 | x, y, 289 | not_x=f'无{result["year"][0]}年关于{name.subject()}的数据', 290 | not_y=f'无{result["year"][0]}前一年关于{name.subject()}的数据' 291 | ): 292 | res = builder.growth_calculation(y.value, x.value) 293 | if builder.add_if_is_not_none( 294 | res, to_sub=False, 295 | no=f'{result["year"][0]}年{name.subject()}的记录非数值类型,无法计算' 296 | ): 297 | answer.add_answer(f'{result["year"][0]}年的{name.subject()}为{y.val()},' 298 | f'其去年的为{x.val()},同比{sign(res, ("降低", "增长"))}{abs(res)}%') 299 | 300 | def make_area_value_ans(self, answer: Answer, builder: AnswerBuilder, 301 | chain: TranslationChain, result: Result): 302 | data = self._search_direct(chain) 303 | builder.feed_data(data) 304 | for item, name in builder.product_data_with_name( 305 | result['area'], result['index'], 306 | if_is_none=lambda _, na: f'{na.subject()}无数据记录' 307 | ): 308 | name.repr = item.repr 309 | answer.add_answer(f'{name.subject()}为{item.value}{item.unit}') 310 | 311 | def make_index_or_area_overall_ans(self, qt: str, answer: Answer, builder: AnswerBuilder, 312 | chain: TranslationChain, result: Result): 313 | if qt == 'index_overall': 314 | gen = [result['index']] 315 | tag = '指标' 316 | else: 317 | gen = [result['area'], result['index']] 318 | tag = '' 319 | data = self._search_double_direct_then_feed(chain) 320 | builder.feed_data(data) 321 | for x, y, f, n in builder.product_data_with_feed( 322 | *gen, 323 | if_x_is_none=lambda _1, _2, _3, na: f'无{na.subject()}的数据记录,无法比较', 324 | if_y_is_none=lambda _1, _2, _3, na: f'无{na.subject()}的父级{tag}数据记录,无法比较' 325 | ): 326 | f.life_check(result['year'][0]) 327 | if not f: 328 | answer.add_answer(f'无{n.subject()}父级{tag}数据记录,无法比较') 329 | return 330 | answer.begin_sub_answers() 331 | unit_x, unit_y = x.unit, y[0].unit 332 | if qt == 'area_overall': # 交换值域 333 | f.area, f.name = f.name, n.name 334 | n.repr = f.repr = x.repr 335 | answer.add_sub_answers(f'{n.subject()}为{x.val()}') 336 | answer.add_sub_answers(f'其父级{tag}{f.subject()}为{y[0].val()}') 337 | if unit_x != unit_y: 338 | answer.add_sub_answers('两者单位不同,无法比较') 339 | answer.end_sub_answers() 340 | return 341 | res1 = builder.binary_calculation(x.value, y[0].value, truediv, percentage=True) 342 | if builder.add_if_is_not_none(res1, no=f'无效的{n.subject()}值类型,无法比较'): 343 | answer.add_sub_answers(f'前者占后者的{res1}%') 344 | res2 = builder.binary_calculation(y[0].value, x.value, truediv) 345 | if builder.add_if_is_not_none(res2, no=f'无效的{n.subject()}值类型,无法比较'): 346 | answer.add_sub_answers(f'后者是前者的{res2}倍') 347 | answer.end_sub_answers() 348 | 349 | def make_index_or_area_2_overall_ans(self, qt: str, answer: Answer, builder: AnswerBuilder, 350 | chain: TranslationChain, result: Result): 351 | years = '、'.join(result['year']) 352 | # init 353 | if qt == 'area_2_overall': 354 | unpack = True 355 | gen = [result["area"], result["index"]] 356 | else: 357 | unpack = False 358 | gen = [result["index"]] 359 | # collect data 360 | data = self._search_double_direct_then_feed(chain, unpack=unpack) 361 | builder.feed_data(data) 362 | # product data 363 | for x, y, f, n in builder.product_data_with_feed( 364 | *gen, 365 | if_x_is_none=lambda _1, _2, _3, na: f'无{years}这几年{na.subject()}的数据记录,无法比较', 366 | if_y_is_none=lambda _1, _2, _3, na: f'无{years}这几年{na.subject()}的父级数据记录,无法比较' 367 | ): 368 | temp = [] # 记录两次计算的结果值 369 | for i, year in enumerate(result["year"]): 370 | f.life_check(year) 371 | if not f: 372 | answer.add_answer(f'无{year}年{n.subject()}的父级数据记录,无法比较') 373 | continue 374 | unit_x, unit_y = x[i].unit, y[i].unit 375 | answer.begin_sub_answers() 376 | n.repr = x[i].repr 377 | answer.add_sub_answers(f'{year}年{n.subject()}为{x[i].value}{unit_x}') 378 | answer.add_sub_answers(f'其总体{f.name}{y[i].repr}为{y[i].value}{unit_y}') 379 | if unit_x != unit_y: 380 | answer.add_sub_answers('两者单位不同,无法比较') 381 | answer.end_sub_answers() 382 | continue 383 | res = builder.binary_calculation(x[i].value, y[i].value, truediv, percentage=True) 384 | if builder.add_if_is_not_none(res, no=f'无效的{n}值类型,无法比较'): 385 | answer.add_sub_answers(f'约占总体的{res}%') 386 | temp.append(res) 387 | answer.end_sub_answers() 388 | if len(temp) == 2: 389 | num = round(temp[0] - temp[1], 2) 390 | answer.add_answer(f'前者相比后者{sign(num, ("降低", "提高"))}{abs(num)}%') 391 | 392 | def make_areas_m_or_n_compare_ans(self, qt: str, answer: Answer, builder: AnswerBuilder, 393 | chain: TranslationChain, result: Result): 394 | data = self._search_direct(chain) 395 | operator = truediv if qt == 'areas_m_compare' else sub 396 | builder.feed_data(data) 397 | for (x, y), (n1, n2) in builder.product_data_with_binary( 398 | result['area'], result['index'], 399 | if_x_is_none=lambda _1, _2, na: f'无{na[0].subject()}数据记录,无法比较', 400 | if_y_is_none=lambda _1, _2, na: f'无{na[1].subject()}数据记录,无法比较' 401 | ): 402 | # 单位检查 403 | ux, uy = x.unit or '无', y.unit or '无' 404 | if builder.add_if_is_equal_or_not(ux, uy, 405 | no=f'{n1.subject()}的单位({ux})与{n2.subject()}的单位({uy})不同,无法比较'): 406 | answer.begin_sub_answers() 407 | n1.repr, n2.repr = x.repr, y.repr 408 | answer.add_sub_answers(f'{n1.subject()}为{x.val()},{n2.subject()}为{y.val()}') 409 | res1 = builder.binary_calculation(x.value, y.value, operator) 410 | if builder.add_if_is_not_none(res1, 411 | no=f'{n1.subject()}或{n2.subject()}非数值类型,无法比较'): 412 | if qt == 'areas_m_compare': 413 | answer.add_sub_answers(f'前者是后者的{res1}倍') 414 | else: 415 | answer.add_sub_answers(f'前者比后者{sign(res1)}{abs(res1)}{ux}') 416 | if qt == 'areas_m_compare': 417 | res2 = builder.binary_calculation(y.value, x.value, truediv) 418 | if builder.add_if_is_not_none(res2, 419 | no=f'{n1.subject()}或{n2.subject()}非数值类型,无法比较'): 420 | answer.add_sub_answers(f'后者是前者的{res2}倍') 421 | answer.end_sub_answers() 422 | 423 | def make_indexes_or_areas_2m_or_2n_compare_ans(self, qt: str, answer: Answer, builder: AnswerBuilder, 424 | chain: TranslationChain, result: Result): 425 | # set operator 426 | if qt in ('areas_2m_compare', 'indexes_2m_compare'): 427 | operator = truediv 428 | else: 429 | operator = sub 430 | # set gen and flatten 431 | if qt in ('areas_2m_compare', 'areas_2n_compare'): 432 | gen = [result['area'], result['index']] 433 | unpack = True 434 | else: 435 | gen = [result['index']] 436 | unpack = False 437 | # code begin 438 | data = self._search_direct(chain, unpack=unpack) 439 | builder.feed_data(data) 440 | for item, name in builder.product_data_with_name(*gen): 441 | x, y = item 442 | if builder.binary_decision( 443 | x, y, 444 | not_x=f'无关于{result["year"][0]}年的{name.subject()}的记录', 445 | not_y=f'无关于{result["year"][1]}年的{name.subject()}的记录' 446 | ): 447 | answer.begin_sub_answers() 448 | res = builder.binary_calculation(x.value, y.value, operator) 449 | if builder.add_if_is_not_none(res, no=f'{name.subject()}的记录为无效的值类型,无法比较'): 450 | answer.add_sub_answers(f'{result["year"][0]}年的{name.subject()}为{x.val()}') 451 | answer.add_sub_answers(f'{result["year"][1]}年的{name.subject()}为{y.val()}') 452 | if qt in ('areas_2m_compare', 'indexes_2m_compare'): 453 | ux, uy = x.unit, y.unit 454 | # 单位为%的数值不支持此类型比较 455 | if ux == uy == '%': 456 | answer.add_sub_answers(f'它们单位为‘%’,不支持此类型的比较') 457 | else: 458 | answer.add_sub_answers(f'前者是后者的{res}倍') 459 | else: 460 | answer.add_sub_answers(f'前者比后者{sign(res, ("减少", "增加"))}{abs(res)}{x.unit}') 461 | answer.end_sub_answers() 462 | 463 | def make_areas_g_compare_ans(self, answer: Answer, builder: AnswerBuilder, 464 | chain: TranslationChain, result: Result): 465 | data = self._search_direct(chain) 466 | builder.feed_data(data) 467 | for item, name in builder.product_data_with_name(result['area'], result['index']): 468 | x, y = item 469 | if builder.binary_decision( 470 | x, y, 471 | not_x=f'无{result["year"][0]}年关于{name.subject()}的数据', 472 | not_y=f'无{result["year"][0]}前一年关于{name.subject()}的数据' 473 | ): 474 | res = builder.growth_calculation(y.value, x.value) 475 | if builder.add_if_is_not_none( 476 | res, to_sub=False, 477 | no=f'{result["year"][0]}年{name.subject()}的记录非数值类型,无法计算' 478 | ): 479 | answer.add_answer(f'{result["year"][0]}年的{name.subject()}为{y.val()},' 480 | f'其去年的为{x.val()},同比{sign(res, ("减少", "增长"))}{abs(res)}%') 481 | 482 | def make_catalog_or_index_change_ans(self, qt: str, answer: Answer, builder: AnswerBuilder, 483 | chain: TranslationChain, result: Result): 484 | data = self._search_direct(chain) 485 | tag_name = '指标' if qt == 'index_change' else '目录' 486 | set1, set2 = set([n.name for n in data[0]]), set([n.name for n in data[1]]) 487 | diff1, diff2 = set1.difference(set2), set2.difference(set1) 488 | n1, n2 = len(diff1), len(diff2) 489 | if builder.add_if_is_equal_or_not( 490 | n1, 0, equal=False, 491 | no=f'{result["year"][1]}年与{result["year"][0]}年的{tag_name}相同' 492 | ): 493 | answer.add_answer(f'{result["year"][1]}年与{result["year"][0]}年相比,未统计{n1}个{tag_name}:' + '、'.join(diff1)) 494 | if builder.add_if_is_equal_or_not( 495 | n2, 0, equal=False, 496 | no=f'{result["year"][0]}年与{result["year"][1]}年的{tag_name}相同' 497 | ): 498 | answer.add_answer(f'{result["year"][0]}年与{result["year"][1]}年相比,未统计{n2}个{tag_name}:' + '、'.join(diff2)) 499 | 500 | def make_catalogs_or_indexes_change_ans(self, qt: str, answer: Answer, chain: TranslationChain, result: Result): 501 | tag = '目录' if qt == 'catalogs_change' else '指标' 502 | data = self._search_direct(chain) 503 | y = [len(item) for item in data] 504 | line = self.painter.paint_line(result['year'], f'{tag}个数', y, result.raw_question) 505 | answer.save_chart(line) 506 | answer.add_answer(f'该问题的回答已渲染为图像,详见:{CHART_RENDER_DIR}/{result.raw_question}.html') 507 | 508 | def make_indexes_or_areas_trend_ans(self, qt: str, answer: Answer, builder: AnswerBuilder, 509 | chain: TranslationChain, result: Result, mark_point: bool = False): 510 | if qt == 'areas_trend': 511 | unpack = True 512 | gen = [result['area'], result['index']] 513 | else: 514 | unpack = False 515 | gen = [result['index']] 516 | # collect 517 | data = self._search_direct(chain, unpack=unpack) 518 | builder.feed_data(data) 519 | collects = [] # 根据不同单位划分数据 520 | for item, name in builder.product_data_with_name(*gen): 521 | collect = [] 522 | units = set([n.unit for n in item if n.unit != '']) 523 | ys = builder.group_mapping_to_float(item) 524 | if builder.add_if_is_equal_or_not(sum(ys), 0, equal=False, 525 | no=f'指标“{name.subject()}”无任何值记录,无法比较'): 526 | for unit in units: 527 | tmp = [] 528 | for y, n in zip(ys, item): 529 | tmp.append(y if n.unit == unit else 0) 530 | collect.append((name.subject(), unit, tmp)) 531 | collects.append(collect) 532 | # paint 533 | if len(collects) != 0: 534 | bar = self.painter.paint_bar(result['year'], collects, 535 | title=result.raw_question, mark_point=mark_point) 536 | answer.save_chart(bar) 537 | answer.add_answer(f'该问题的回答已渲染为图像,详见:{CHART_RENDER_DIR}/{result.raw_question}.html') 538 | 539 | def make_indexes_or_areas_overall_trend_ans(self, qt: str, answer: Answer, builder: AnswerBuilder, 540 | chain: TranslationChain, result: Result): 541 | if qt == 'areas_overall_trend': 542 | unpack = True 543 | gen = [result["area"], result["index"]] 544 | else: 545 | unpack = False 546 | gen = [result["index"]] 547 | data = self._search_double_direct_then_feed(chain, unpack) 548 | builder.feed_data(data) 549 | parents = {} 550 | children = {} 551 | for x, y, f, n in builder.product_data_with_feed( 552 | *gen, 553 | if_x_is_none=lambda _1, _2, _3, na: f'无关于”{na.subject()}“的记录', 554 | if_y_is_none=lambda _1, _2, _3, na: f'无关于”{na.subject()}“的父级记录' 555 | ): 556 | xs = builder.group_mapping_to_float(x) 557 | if not builder.add_if_is_not_none(xs, to_sub=False, 558 | no=f'{n.subject()}的记录非数值类型,无法比较'): 559 | return 560 | parent = f.name + n.name if qt == 'areas_overall_trend' else f.name 561 | ys = builder.group_mapping_to_float(y) 562 | if not builder.add_if_is_not_none(ys, to_sub=False, 563 | no=f'{n.subject()}的父级记录({parent})非数值类型,无法比较'): 564 | return 565 | overall = [round(i / j, 3) if j != 0 else 0 for i, j in zip(xs, ys)] 566 | # 同一个父级指标将其子孙合并 567 | parents[parent] = ys 568 | children.setdefault((parent, x[-1].unit), []).append((n.subject(), xs, overall)) 569 | # paint 570 | if len(parents) != 0: 571 | for bar in self.painter.paint_bar_stack_with_line(result['year'], children, parents, 572 | result.raw_question): 573 | answer.save_chart(bar) 574 | answer.add_answer(f'该问题的回答已渲染为图像,详见:{CHART_RENDER_DIR}/{result.raw_question}.html') 575 | 576 | def make_indexes_or_areas_max_ans(self, qt: str, answer: Answer, builder: AnswerBuilder, 577 | chain: TranslationChain, result: Result): 578 | # 可以直接复用 579 | if qt == 'areas_max': 580 | self.make_indexes_or_areas_trend_ans('areas_trend', answer, builder, chain, result, mark_point=True) 581 | else: 582 | self.make_indexes_or_areas_trend_ans('indexes_trend', answer, builder, chain, result, mark_point=True) 583 | 584 | def make_begin_stats_ans(self, answer: Answer, builder: AnswerBuilder, 585 | chain: TranslationChain, result: Result): 586 | data = self._search_direct(chain) 587 | builder.feed_data(data) 588 | for item, name in builder.product_data_with_name(result['index']): 589 | years = [int(year.name) for year in item] 590 | answer.add_answer(f'指标“{name.name}”最早于{min(years)}年开始统计') 591 | --------------------------------------------------------------------------------