资讯动态

Python豆瓣电影数据分析可视化系统源码:Flask+ECharts全链路课程设计

发布时间:2026/10/24 3:07:59 来源:尧图企业网站定制
简介面向Python毕业设计与数据分析入门者的豆瓣电影数据分析及可视化系统完整工程采用Flask后端搭配ECharts前端图表配合爬虫与pandas完成从数据采集、清洗处理到多维动态可视化的闭环数据库基于MySQL可用于毕业设计答辩、课程实训或数据分析练手。压缩包共1054个文件、约30.07MB以688个Python源码为主另有pyc缓存、HTML/JS/CSS前端页面、SQL建库脚本、CSV数据样本、JPG与PNG图表素材以及DOCX论文文档等环境依赖的wheel与exe一并附带。目前已有256人学习下载。整套代码结构完整、模块划分清晰涵盖爬虫抓取目标站点、数据清洗入库、接口调用与图表渲染各环节可据此理解多角度动态视图的实现方式与用户检索识别率的优化思路并结合论文文档完成选题撰写与二次开发。1. 从一份能跑通的课程设计源码说起答辩前一晚我把这套基于 Python 的豆瓣电影数据分析及可视化系统在本机上从头跑了一遍爬虫抓完一批电影条目pandas 清洗后写进 MySQLFlask 起服务浏览器里 ECharts 把评分分布、类型占比、年度趋势一张张画出来。它解决的核心问题是“从原始网页到一张能讲的图表整条链路能不能一次跑通”——采集、清洗、存储、接口、前端五段都在同一个仓库里改一个字段不用跨三个工程去找。这份资源适合三类人正在定 Python 毕业设计选题、需要一个能当场演示完整链路的人想练 pandas 分组聚合 Flask 接口 ECharts 配置的新手以及需要一份带建表 SQL 和论文骨架做参照的人。源码、SQL 脚本、论文三样齐全意味着表结构不用从零设计分析结论也不用凭空编。2. 技术栈与目录结构Flask ECharts pandas 是怎样分层组织的拿到源码先别急着pip install花十分钟把分层看清楚后面改起来会顺很多。这个项目的结构是典型的“脚本 Web”混合体离线侧跑爬虫和 pandas 计算在线侧只负责查库和吐 JSON两边通过 MySQL 这张“中间层”解耦。理解了这条边界你就知道什么问题该去改哪个文件。2.1 四层技术选型为什么是 Flask 而不是 Django先看技术栈的职责划分层次选型在这个项目里的实际职责采集层requests BeautifulSoup请求列表页、翻页、解析片名/评分/类型等字段落 CSV计算层pandas numpy缺失值处理、类型拆分、分组聚合产出各维度统计结果存储层MySQL PyMySQL电影主表、短评表、聚合结果表作为离线与在线之间的交换区展示层Flask Jinja2 ECharts路由渲染页面、接口返回 JSON、前端配置 option 画图Flask 在这里是合理选择。毕业设计的体量就几十个接口不到Django 自带的那套 ORM、admin、中间件对这么小的项目是负担学生往往花两天时间跟 settings 搏斗真正想讲的分析逻辑反而没时间打磨。Flask 十几个路由加两个蓝图就能把事办完jsonify直接返回字典前端拿到就能塞进 ECharts 的series.data中间不需要任何序列化层。另一个容易被问到的点是为什么不用 pyecharts 直接生成图我一般不建议。pyecharts 产出的是静态 HTML 片段做单图好看但一旦要做“点击某个年份下面两张图联动刷新”这种交互就得绕回去手写 JS。把 ECharts 的option握在自己手里虽然多写几十行配置但调试和改造的空间大得多答辩演示时也更像“自己做的”。pandas 承担计算而不是把聚合全推给 SQL同样有现实考量。分组、透视、分箱这些操作在 pandas 里三五行走完写成 SQL 要嵌套子查询更关键的是清洗阶段本来就在 pandas 里算完直接to_sql入库不需要在两个工具之间反复搬数据。调试期还可以先落 CSV 看中间结果比对着数据库猜哪一步出错省事得多。2.2 目录结构与文件职责源码包拆开看把压缩包解开主干大概是这样不同版本可能多几个文件主干逻辑一致douban_movie_analysis/ ├── app.py # 应用入口create_app 蓝图注册 ├── config.py # 数据库连接、爬虫入口、分页参数 ├── requirements.txt # 依赖清单照它装环境 ├── crawler/ │ ├── spider.py # 请求、翻页、重试、落 CSV │ └── parser.py # 单页 HTML 解析字段抽取规则集中在这里 ├── analysis/ │ ├── clean.py # pandas 清洗类型转换、去重、拆分多值字段 │ └── stats.py # 聚合评分分布、类型榜、年度趋势 ├── models/ │ └── movie.py # 建表 DDL 与查询封装原生 SQL 为主 ├── views/ │ ├── page.py # 页面路由渲染 Jinja2 模板 │ └── api.py # 数据接口统一返回 JSON ├── static/ │ ├── js/echarts.min.js # 本地化 ECharts避免离线演示翻车 │ └── css/style.css ├── templates/ │ ├── index.html # 首页概览卡片 两张主图 │ └── dashboard.html # 详情页多图联动 ├── sql/ │ └── init.sql # 建库建表脚本导入即用 └── data/ └── movie_raw.csv # 爬虫产出的原始数据清洗的输入有几个细节值得留意。parser.py单独拆出来是因为站点页面结构一变你只需要改这一个文件spider.py里的请求与翻页逻辑完全不用动这是把“易变的部分”隔离出去的基本操作。static/js/echarts.min.js放本地而不是挂 CDN答辩教室网络不稳的时候这一条能救你一命——这类玄学问题我吃过不止一次亏。数据流是一条单向管道spider.py写data/movie_raw.csv→clean.py读 CSV 清洗 →stats.py聚合 → 结果写 MySQL →views/api.py查库返回 JSON → 前端 ECharts 渲染。任何一环出问题都可以单独拎出来验证不用整条链路重跑。2.3 数据库结构设计三张表承接整条数据流sql/init.sql里最核心的是电影主表其他分析都从它派生CREATE TABLE movie ( id INT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 自增主键, douban_id VARCHAR(16) NOT NULL COMMENT 站点条目ID去重的唯一依据, title VARCHAR(128) NOT NULL COMMENT 片名, year SMALLINT DEFAULT NULL COMMENT 上映年份年度趋势图用, rating DECIMAL(3,1) DEFAULT NULL COMMENT 评分 0.0-10.0, rating_count INT UNSIGNED DEFAULT 0 COMMENT 评价人数热度维度用, country VARCHAR(64) DEFAULT NULL COMMENT 制片国家多值用 / 分隔, genres VARCHAR(128) DEFAULT NULL COMMENT 类型标签多值用 / 分隔, summary TEXT COMMENT 简介列表页拿不到时留空, PRIMARY KEY (id), UNIQUE KEY uk_douban_id (douban_id), KEY idx_year (year), KEY idx_rating (rating) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT电影主表;参数上有几个点必须讲清楚。douban_id上建唯一索引是为了让爬虫可以反复重跑而不产生重复数据——配合后面入库时的INSERT IGNORE或ON DUPLICATE KEY UPDATE补爬增量数据时才不会把表撑成两倍。rating用DECIMAL(3,1)而不是FLOAT因为评分要参与区间统计比如“8.0 分以上有多少部”浮点误差会让边界值落到错误的桶里这种 bug 排查起来极其难受。year和rating各建一个普通索引年度趋势和评分分布两个接口是访问最频繁的几十万行数据下索引有没有差别很明显。字符集统一utf8mb4片名里带生僻字或者特殊符号时不会插入失败。另外两张表一张是短评/评论表comment存movie_id、content、votes供词云或情感倾向分析用一张是聚合结果表stat_summary存dimension、dim_key、metric、value把算好的统计结果固化下来。聚合表的意义在于页面打开时不用每次重新扫全表算一遍直接查结果响应从几百毫秒降到几毫秒。3. 数据采集与清洗从列表页到 DataFrame 的完整链路这一章是整个项目里最容易翻车的部分也是最能在答辩时讲出东西的部分。很多同学的做法是爬完直接入库中间不做校验结果图表出来全是“未知类型”“0 分电影”自己都不知道数据在哪一步被污染了。稳妥的做法是把采集、解析、清洗拆成三个可独立运行的环节每步都有可检查的中间产物。3.1 请求与翻页Session 复用、节流与重试爬虫的第一原则是别把自己搞封。下面这段是spider.py的主循环骨架import time, random, requests from parser import parse_list_page from config import SPIDER_BASE_URL, PAGE_START, PAGE_END HEADERS { User-Agent: (Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36), Accept-Language: zh-CN,zh;q0.9, } def fetch(session, url, retry3): 带重试的单页抓取失败返回 None 而不是抛异常打断整轮 for i in range(retry): try: resp session.get(url, headersHEADERS, timeout10) if resp.status_code 200: resp.encoding resp.apparent_encoding # 中文页面防乱码 return resp.text print(f[warn] status{resp.status_code} url{url} 第{i1}次) except requests.RequestException as e: print(f[warn] {e} url{url} 第{i1}次) time.sleep(random.uniform(2, 5)) # 退避别贴着打 return None def crawl(): session requests.Session() # 复用连接别每次新建 for page in range(PAGE_START, PAGE_END 1): url f{SPIDER_BASE_URL}?start{page * 25}filter html fetch(session, url) if not html: continue # 单页失败跳过不中断 for row in parse_list_page(html): yield row time.sleep(random.uniform(1.5, 3.5)) # 页间随机间隔逻辑上有三处是有意为之。Session复用连接池比每页新建requests.get快一截也让 Cookie 在会话内自然保持。fetch捕获异常并返回None而不是让异常冒到主循环——单页失败跳过整轮还能跑完产出的 CSV 只是少几条不会前功尽弃。退避用random.uniform而不是固定sleep(1)固定间隔的请求节奏太规整容易被判定为脚本流量。参数怎么调timeout10是折中值太短会把慢响应误判为失败太长会让整轮耗时不可控retry3配合 25 秒退避覆盖绝大多数瞬时失败。PAGE_START/PAGE_END放在config.py里第一次跑建议只抓 23 页验证解析规则对不对确认字段齐全再放开页数别一上来就抓几百页等半小时发现字段全空。3.2 字段解析把多值字段的处理规则定死parser.py负责单页 HTML 到字典的映射写法上重点是把“取不到怎么办”说清楚from bs4 import BeautifulSoup def _text(node, default): return node.get_text(stripTrue) if node else default def parse_list_page(html): soup BeautifulSoup(html, lxml) rows [] for item in soup.select(ol.grid_view li): link item.select_one(div.hd a) info _text(item.select_one(div.bd p)) # 1994 / 美国 / 犯罪 剧情 rating _text(item.select_one(span.rating_num)) votes _text(item.select_one(div.star span:last-child)) # 2800000人评价 parts [p.strip() for p in info.split(/) if p.strip()] rows.append({ douban_id: link[href].split(/)[-2] if link and link.get(href) else , title: _text(link), year: parts[0] if parts and parts[0].isdigit() else , country: parts[1] if len(parts) 1 else , genres: parts[2].replace( , /) if len(parts) 2 else , rating: rating, rating_count: votes.replace(人评价, ).replace(,, ), }) return rows关键在于“不假设结构完整”。真实页面上有些条目缺年份、有些缺国家、有些简介为空直接parts[0]、parts[1]索引会在某一条上抛IndexError整页数据全丢。用长度判断加默认空串脏数据留到清洗阶段统一处理比在解析层做严格校验现实得多。genres的replace( , /)是刻意做的一步页面上一部电影的类型写成“犯罪 剧情”空格分隔入库后前端要按类型聚合就没法拆。统一换成/分隔pandas 里一个str.split(/)加explode()就能展开成多行类型榜才好算。这类“在边界处统一格式”的习惯越早建立越省事。3.3 pandas 清洗类型转换、去重与多值展开拿到原始数据后clean.py做的是把字符串变成能算的数import pandas as pd, numpy as np def clean(df: pd.DataFrame) - pd.DataFrame: df df.copy() df[rating] pd.to_numeric(df[rating], errorscoerce) # 脏值变 NaN df[rating_count] pd.to_numeric(df[rating_count], errorscoerce).fillna(0).astype(int) df[year] pd.to_numeric(df[year], errorscoerce) df[year] df[year].where(df[year].between(1900, 2030)) # 越界年份置空 df df.dropna(subset[rating, year]) # 分析必需字段 df df[df[douban_id].astype(str).str.len() 0] df df.drop_duplicates(subset[douban_id], keeplast) # 后抓的覆盖先抓的 df[genres] df[genres].fillna(未知).replace(, 未知) return df.reset_index(dropTrue) def explode_genres(df: pd.DataFrame) - pd.DataFrame: 类型是多值字段展开成一行一类型才能做类型维度聚合 g df.assign(genredf[genres].str.split(/)).explode(genre) return g[g[genre].str.len() 0]errorscoerce是这段的核心参数遇到“暂无评分”这类非数值直接转成NaN而不是抛异常再配合后面的dropna把无法参与统计的行剔掉。如果这里用astype(float)一条脏数据就能让整个脚本崩掉。年份的between(1900, 2030)过滤看着多余实际很有必要——列表页偶尔会把“2024”错位解析成别的数字不设上下界的话年度趋势图 X 轴会被一个 9999 拉爆整个图挤成一条线这种视觉上的“玄学”问题往往是数据侧没兜住。drop_duplicates(subset[douban_id], keeplast)保留最后一条配合重复爬取等价于一次轻量的“更新覆盖”。3.4 落库与聚合表把结果固化成查询友好的形状清洗完的 DataFrame 直接写库注意引擎和参数from sqlalchemy import create_engine import pandas as pd engine create_engine( mysqlpymysql://user:pass127.0.0.1:3306/movie_db?charsetutf8mb4, pool_pre_pingTrue, pool_recycle3600 ) def save_movies(df: pd.DataFrame): cols [douban_id, title, year, rating, rating_count, country, genres] df[cols].to_sql(movie, engine, if_existsappend, indexFalse, methodmulti) def build_summary(df: pd.DataFrame, engine): # 评分分布按 0.5 分一档分箱 bins [i * 0.5 for i in range(0, 21)] dist pd.cut(df[rating], binsbins, rightFalse).value_counts().sort_index() stat pd.DataFrame({ dimension: rating_dist, dim_key: [f{b.left:.1f}-{b.right:.1f} for b in dist.index], metric: count, value: dist.values, }) stat.to_sql(stat_summary, engine, if_existsappend, indexFalse)charsetutf8mb4必须写在连接串里只在建表时指定字符集是不够的——连接层的字符集不对中文片名照样变问号。methodmulti让 pandas 拼多值INSERT几万行数据比逐条插入快一个量级。聚合表的设计是“维度 键 指标 值”的长表结构好处是所有图表接口统一成一条 SQLSELECT dim_key, value FROM stat_summary WHERE dimension %s。加一个新图表只要在stats.py里多算一个维度、多插一批行api.py和前端都不用改结构。这种“用数据换代码”的取舍在小项目里往往比精心设计的宽表更实用。4. Flask 接口与 ECharts 对接一条 JSON 从 SQL 到图表的路径后端这部分代码量不大但结构清不清楚直接决定你后期加图表是五分钟还是五小时。这个项目用的是应用工厂加蓝图接口和页面分开好处是接口可以脱离页面单独测——用浏览器直接开/api/rating_dist看到 JSON 就说明后端没问题剩下的锅在前端。4.1 应用工厂与蓝图注册from flask import Flask from views.page import page_bp from views.api import api_bp from config import DB_URI def create_app(): app Flask(__name__) app.config.from_mapping( SQLALCHEMY_DATABASE_URIDB_URI, JSON_AS_ASCIIFalse, # 中文不转 \uXXXX调试时看得清 CACHE_TTL300, # 聚合接口缓存 5 分钟 ) app.register_blueprint(page_bp) # 页面/、/dashboard app.register_blueprint(api_bp, url_prefix/api) return app if __name__ __main__: create_app().run(host127.0.0.1, port5000, debugTrue)JSON_AS_ASCIIFalse是个小配置但很影响体验不然接口返回的中文类型名全是\u5267\u60c5这种前端图表标签得再解码一次。url_prefix/api把所有数据接口收在一个前缀下前端fetch拼接统一将来要换域名或加重写规则也简单。debugTrue方便热重载但有个副作用后面第 5 章会细说重载器会 fork 两个进程如果你把爬虫或聚合逻辑写在app.py顶层会被执行两次。稳妥做法是所有初始化动作都放进__main__的判断里或者单独拆成脚本跑。4.2 典型接口实现参数化查询与空结果兜底from flask import Blueprint, jsonify, request from sqlalchemy import create_engine, text from config import DB_URI api_bp Blueprint(api, __name__) engine create_engine(DB_URI, pool_pre_pingTrue) api_bp.route(/rating_dist) def rating_dist(): sql text( SELECT dim_key AS label, value AS num FROM stat_summary WHERE dimension rating_dist ORDER BY dim_key ) with engine.connect() as conn: rows [dict(r._mapping) for r in conn.execute(sql)] return jsonify({code: 0, data: rows}) api_bp.route(/genre_top) def genre_top(): limit request.args.get(limit, 10, typeint) # 默认 10带类型转换 limit max(1, min(limit, 50)) # 上限兜底防大查询 sql text( SELECT dim_key AS genre, value AS cnt FROM stat_summary WHERE dimension genre_top ORDER BY value DESC LIMIT :limit ) with engine.connect() as conn: rows [dict(r._mapping) for r in conn.execute(sql, {limit: limit})] return jsonify({code: 0, data: rows})统一返回{code: 0, data: [...]}的结构前端拿res.data直接用不需要每张图写一套解析逻辑。text()加命名参数是防注入的标准写法别用 f-string 拼LIMIT这类代码在答辩时被问到很难解释。limit max(1, min(limit, 50))这一行是给自己留的后悔药前端传limit100000的时候接口不会真的去扫全表。这类边界检查在新手项目里经常被忽略等到数据量涨上来一个参数就能把服务拖死。4.3 ECharts 前端配置option 结构与容器高度// templates/index.html 中先给容器定死高度再画图 async function renderGenreChart() { const dom document.getElementById(genreChart); const res await fetch(/api/genre_top?limit10).then(r r.json()); const rows res.data || []; const chart echarts.init(dom); chart.setOption({ title: { text: 高分电影类型 TOP10 }, tooltip: { trigger: axis }, grid: { left: 3%, right: 6%, bottom: 3%, containLabel: true }, xAxis: { type: category, data: rows.map(i i.genre) }, yAxis: { type: value, name: 部数 }, series: [{ type: bar, data: rows.map(i i.cnt), itemStyle: { borderRadius: [4, 4, 0, 0] }, // 空数据时给一个占位避免页面一片空白无从排查 emptyCircleStyle: { color: #ccc } }] }); window.addEventListener(resize, () chart.resize()); // 自适应 }grid.containLabel: true解决的是标签被裁掉的问题中文类目名稍长就会顶出画布。window.addEventListener(resize)那行单独拿出来说不加它窗口一缩放图表就错位演示时全屏切换必然出现属于典型的“现场翻车”点。容器高度在 CSS 里必须显式写死比如.chart { width: 100%; height: 420px; }。ECharts 初始化时读取容器尺寸父元素高度是auto且内容为空算出来就是 0图画出来是空白的——这个现象看起来像数据没拿到实际是布局问题排查时先看 DOM 高度再看接口返回。5. 本地跑通与避坑环境、依赖和五类高频翻车5.1 最小可运行步骤按这个顺序走每一步都有可见产物出问题好定位# 1. 建虚拟环境装依赖版本以 requirements.txt 为准 python -m venv venv source venv/bin/activate # Windows: venv\Scripts\activate pip install -r requirements.txt # 2. 建库建表 mysql -uroot -p sql/init.sql # 3. 改配置config.py 里的 DB_URI、SPIDER_BASE_URL、PAGE_END # 第一次跑把 PAGE_END 设成 2先验证解析规则 # 4. 采集与清洗分两步跑中间产物可检查 python -m crawler.spider # 产出 data/movie_raw.csv python -m analysis.stats # 清洗 聚合 写 MySQL # 5. 起服务 python app.py # 访问 http://127.0.0.1:5000判断标准很简单第 4 步之后data/movie_raw.csv打开应该有中文片名数据库SELECT COUNT(*) FROM movie应该大于 0访问/api/rating_dist应该返回一屏 JSON。三段都过了再去开页面能把问题范围缩得很小。5.2 坑一数据库里中文全是问号现象页面图表标签显示“??”或者数据库里title字段查出来是???但 CSV 里明明是正常中文。原因多半是连接层字符集没设。建表语句写了utf8mb4但 PyMySQL 的连接默认字符集可能是latin1写入时就被转坏读出来自然是问号。也有可能是 MySQL 服务端character_set_server不是 utf8mb4。解决连接串显式加?charsetutf8mb4如果还不行在init.sql里补一句SET NAMES utf8mb4;并检查my.cnf中character-set-server配置。改完必须重新导入一次数据——已经被转坏的字符救不回来。5.3 坑二爬虫第一页就有数据第二页开始全是空现象movie_raw.csv只有二十几行或者字段大部分为空但脚本没报错。原因两类。一是翻页参数拼错start递增的步长和列表页实际每页条数不匹配请求的还是同一页或者跳到越界页二是parser.py的选择器写死了某个类名页面结构一变select_one全部返回None而_text给了默认空串静默吞掉了错误。解决先把抓到的 HTML 存一份到本地文件用BeautifulSoup在 REPL 里逐条试选择器翻页那行start{page * 25}里的 25 要和页面实际条数对齐。更重要的是在解析后加一行断言比如assert len(rows) 0, 本页解析为空检查选择器让错误立刻暴露而不是攒一堆空数据到清洗阶段才发现。5.4 坑三图表一片空白控制台还报 option 未定义现象页面能打开标题和导航都在图表区域是纯白色F12 控制台报echarts is not defined或者Cannot read properties of null。原因echarts is not defined说明echarts.min.js没加载成功——常见于 CDN 被墙或路径写错Cannot read properties of null说明init拿到的容器不存在通常是id拼错或者脚本在 DOM 渲染完成之前就执行了。解决把echarts.min.js放static/js/走本地路径彻底摆脱网络依赖脚本用DOMContentLoaded包一层或者把script放到/body前顺手确认容器id前后一致。这一条看着低级但真到了答辩现场网络抖动加上路径笔误两个问题叠在一起排查会很痛苦。5.5 坑四pandas 写库报nan相关的类型错误现象to_sql抛OperationalError或者ProgrammingError提示列类型不匹配或者 MySQL 报Incorrect integer value: nan。原因pandas 里的NaN浮点空值在转 SQL 时不会自动变成NULL尤其整型列里混进NaN会被转成字符串nan或者直接报错。不同版本的 pandas SQLAlchemy 组合行为还不一样。解决入库前显式替换df df.where(pd.notnull(df), None)或者对整型列先astype(Int64)可空整型再落库。实在调不通就先落 CSV用LOAD DATA或INSERT IGNORE手动导别在类型转换上死磕——毕业设计阶段能跑通比写法优雅重要。5.6 坑五debug 模式下爬虫被执行两次现象python app.py之后控制台日志成对出现数据表里的行数正好是预期的两倍或者聚合结果重复累加。原因Flask 的debugTrue会开启重载器父进程和子进程各执行一次模块顶层代码。如果你把爬虫调用或build_summary写在了create_app()外面的模块级语句里就会被跑两遍。解决所有初始化动作放进if __name__ __main__:里面或者用os.environ.get(WERKZEUG_RUN_MAIN) true做判断。更干净的做法是把采集和聚合彻底拆成独立脚本本项目就是这么做的Web 进程只负责查库职责单一重复执行的问题自然消失。6. 二次开发把课程设计改成能讲清楚的项目要把它从“能跑”推到“能讲”改三处性价比最高。第一处是给聚合加维度比如在stats.py里按“年代段 × 类型”做交叉透视pd.crosstab三行就能出结果前端加一张堆叠柱状图答辩时能主动说“我发现 90 年代之后犯罪类型占比明显上升”比念 PPT 强得多。第二处是把短评表接进来做词频统计jieba分词加停用词表产出高频词给词云工程量不大但视觉效果加分。第三处是给接口加缓存用functools.lru_cache或者内存字典加 TTL把聚合查询的耗时压下去。改造后一定要做数据对账别只看图好不好看。我会写一个小脚本把接口返回的数值和数据库里 SQL 直接算的结果比一遍校验项校验方式通过标准总量一致性SELECT COUNT(*) FROM movievs 评分分布各档求和两者完全相等类型榜接口返回的 TOP10 之和 vsstat_summary该维度总和接口是子集不超总数年份边界检查year的最大最小值落在 19002030 之间空值比例统计rating为NULL的行占比低于 5% 才适合直接上图对不上的时候先查清洗阶段丢了多少行再看聚合口径是不是前后不一致最后才怀疑前端。我头几次改完图就直接截图存论文结果发现某张图的合计比总数少了三成原因是explode之后忘了去掉空类型白折腾了一晚上。从那以后我每次改完聚合逻辑都强制走一遍“脚本对账 → 接口比对 → 页面截图”这三步哪怕时间再紧也不跳。做课设最怕的不是功能少是自己都说不清图上的数字是怎么来的。这套源码最大的价值也在这里——链路完整、每段都能单独验证、SQL 和论文骨架都给了你只需要在它上面加一层自己的东西。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑