简介:统计掉线率最高的前10基站,这份资源提供了从原始通话记录到最终排名的完整数据与查询脚本。面向通信网络质量分析、基站运维人员,以及正在学习Hive与MySQL数据分析的读者,可帮助理解如何基于通话明细计算掉线率,并准确筛选出需要重点优化的高掉线基站。压缩包共2个文件,包括1个CSV格式的原始数据文件和1个MySQL SQL脚本,整体约13.03MB。数据文件包含通话时间、基站编号、手机编号、掉话秒数和通话总秒数等核心字段,SQL脚本则实现了数据载入、汇总统计以及按掉线率排序输出前10基站的功能,逻辑清晰便于直接复用。已有242人学习下载,适合需要快速定位高掉线基站、开展网络质量分析与优化工作的工程师和学习者,也可将CSV导入Hive环境做进一步探索,与MySQL版本形成互补。
1. 这个 7z 包里到底装了什么:CDR、IMEI、小区信息,一次说清
第一次看到cdr_summ_imei_cell_info(csv-mysql).7z这个名字,大多数人会直接解压,找里面那份 MySQL dump 到底能不能用。这个思路不算错,但我建议先停下来把后缀拆开读一遍:cdr_summ 是 CDR(呼叫详细记录)的摘要,imei_cell_info 表示维度落在设备 IMEI 和基站小区上,csv-mysql 说的是同时交付两种格式——CSV 平面文件和 MySQL 表结构与数据,7z 只是最后的压缩壳。它解决的是非常具体的一类需求:原始 CDR 一天几个 GB,业务方不要每一条呼叫,而要“某个设备某一天出现在哪些小区、打了多少电话、累计通话多久”。
这个包适合三类人:做终端与网络质量分析的数据工程师,想把 IMEI 和小区轨迹对应起来;做基站负载和资源调度的网优人员,需要按 LAC/CI 聚合出小区每日业务量;以及做数据交付的平台开发,要把“CSV + MySQL”双格式的离线任务打包好丢给下游。如果你接手的是别人交付的这类包,它本质上是一个规范化后的接口产物,不是原始详单。
下面不重复什么神秘原稿,我按做 CDR 汇总交付最常见的技术路径,把包的内容结构、复现方式、入库细节和坑一一拆开。看完后你既能判断手里的包能不能直接入库,也能自己从原始 CDR 造出一份同样的 7z 交付物。
2. 包里的两样东西:CSV 文件管分析,MySQL 转储管还原
一个规范的 CDR 汇总交付包,内容清单通常不复杂:一个 CSV 汇总表,一个 SQL 表结构文件,可能还有一个数据转储文件,再加一份 README。标题把 csv 和 mysql 并列,不是让你二选一,而是两个出口同时存在。CSV 给数据分析师直接读,MySQL 给要跑 SQL 的工程师。两边数据应当来自同一次计算,不应该出现同一时间段两边行数对不上的情况。
2.1 别被文件名骗了:IMEI 不等于手机号,小区 ID 才是位置锚点
在 CDR 原始字段里,IMEI 是终端设备标识,跟手机号不是一一对应。同一张卡换到另一台手机,IMSI 和手机号不变,但 IMEI 会变;同一个 IMEI 也可能被不同卡用过。所以按 IMEI 汇总时,你统计的是“设备”的行为,不是“用户”的行为。很多分析翻车,就是因为把 IMEI 直接当成用户 ID 去做人群画像。
小区信息一般由 MCC、MNC、LAC、Cell ID 四个字段共同刻画,也就是常说的 CGI。很多 CDR 表里只有 cell_id 一个字段,这在单省单网内够用,跨省或者跨运营商就冲突了——不同地方的小区 ID 会重复。我拿到这种包的第一件事,是确认 CSV 里有没有完整保留 mcc、mnc、lac、cell_id 四列。如果只有 cell_id,那这份摘要大概率只能做省内粗分析,不能当全局位置锚点。
| 字段 | 含义 | 注意点 |
|---|---|---|
| imei | 国际移动设备识别码 | 15 位数字,必须按字符串处理 |
| imsi | 国际移动用户识别码 | 跟着 SIM 卡走,不跟设备走 |
| msisdn | 手机号 | 可能为空或被脱敏 |
| mcc / mnc | 国家码 / 网络码 | 基站全局标识的前两段 |
| lac | 位置区码 | 同一区域内的寻呼区 |
| cell_id | 小区标识 | 不同 LAC 下可能重复 |
完整基站标识是 mcc + mnc + lac + cell_id 四项拼起来,只看 cell_id 是很多后续统计对不上的根源。所以先看表头,再验证字段,再谈入库。
2.2 用 Python 快速验证包里的 CSV:先统计再信它
解压之后第一步不是急着导 MySQL,而是用脚本把 CSV 的基本形状拉出来。下面是我常写的最小验证脚本,依赖 pandas,但只需要几十行。
import pandas as pd df = pd.read_csv( "cdr_summ_imei_cell_info.csv", dtype={ "imei": str, "mcc": str, "mnc": str, "lac": str, "cell_id": str, }, parse_dates=["first_seen", "last_seen"], ) print("shape:", df.shape) print("IMEI 数:", df["imei"].nunique()) print("小区数:", df.groupby(["mcc", "mnc", "lac", "cell_id"]).ngroups) # 按小区看总业务量,验证数据不是空壳 cell_stat = ( df.groupby(["mcc", "mnc", "lac", "cell_id"], as_index=False) .agg(device_count=("imei", "nunique"), total_calls=("call_count", "sum")) .sort_values("total_calls", ascending=False) ) print(cell_stat.head(10))dtype 里所有 ID 字段都强制按字符串读,这是防止 pandas 把 IMEI 当 int64 导致精度丢失的关键。parse_dates 只对时间戳字段生效,如果 CSV 里的时间列是字符串而且格式不规范,需要先确认格式再转。groupby 用四元组做校验,如果 cell_id 单独的唯一数和四元组唯一数一致,说明小区 ID 没有跨 LAC 重复;如果不一致,后续按小区聚合就得保留完整四元组。
2.3 MySQL 转储怎么看:它和 CSV 是同一条账,不是另一套数据
MySQL 出口可能是一份 schema.sql 加一份 dump.sql,也可能是一个可执行的 SQL 文件同时包含建表和 INSERT 语句。它与 CSV 应当是同一份汇总数据,只是格式不同。看包之前,先确认压缩包内部结构,不要上来就把整个 SQL 灌进数据库。
# 文件名里有括号,建议用双引号包住 7z l "cdr_summ_imei_cell_info(csv-mysql).7z" 7z x "cdr_summ_imei_cell_info(csv-mysql).7z" -o/tmp/cdr_pkg less /tmp/cdr_pkg/schema.sql7z l 只列清单不落地,先看清楚包内有几个文件、有没有 README。7z x 解压并指定输出目录,-o 后面直接跟目录名,注意不要加空格。文件名里的括号在 shell 里有特殊语义,双引号是最稳的写法。解压后先看 schema.sql 的建表语句,确认字段名、字符集、主键设计,再决定是否导入,尤其别在生产库里直接跑别人给的 dump。
3. 自己复现这套流程:CDR 原始表到 IMEI-小区汇总 CSV
没有现成包时,从原始 CDR 再造一份也完全可行。不同厂家 CDR 的列名不一样,但只要包含时间戳、IMEI、CGI 相关字段和通话时长,下面这套流程就能跑通。核心工作可以拆成三块:字段清洗、按维度聚合、压缩成 7z。
3.1 最小聚合脚本:按 IMEI x 小区 x 天 出摘要
先用 pandas 写一个最小可跑版本,适合原始数据在一个中等规模 CSV 文件里的情况。
import pandas as pd SRC = "cdr_raw.csv" OUT = "cdr_summ_imei_cell_info.csv" raw = pd.read_csv( SRC, dtype={"imei": str, "mcc": str, "mnc": str, "lac": str, "cell_id": str}, parse_dates=["start_time"], ) # 只保留移动主叫和被叫语音,去掉短信、数据、失败呼叫 raw = raw[raw["call_type"].isin(["MO_CALL", "MT_CALL"])] # 按自然日切分,而不是保留时分秒 raw["date"] = raw["start_time"].dt.strftime("%Y-%m-%d") agg = ( raw.groupby(["mcc", "mnc", "lac", "cell_id", "imei", "date"], as_index=False) .agg( call_count=("duration_sec", "size"), duration_sum=("duration_sec", "sum"), ) ) agg.sort_values(["date", "call_count"], ascending=[True, False]).to_csv( OUT, index=False, encoding="utf-8-sig" )这段脚本的关键在于聚合粒度:mcc、mnc、lac、cell_id 四个字段合起来才是“某个小区”,imei 是设备维度,date 是时间分区。按这个粒度输出,一条记录表示“某个设备某一天在某个小区打了多少次电话、总时长多少秒”。call_type 的过滤很重要,很多原始 CDR 里短信、呼叫转移、失败呼叫混在一起,不过滤会把 call_count 放大。encoding 用 utf-8-sig 是为了让 Excel 直接打开不乱码;如果下游全是 Linux 环境,可以改回 utf-8。
3.2 数据量大到爆内存:用 DuckDB 的 SQL 聚合兜底
pandas 面对几亿行 CDR 很吃力,groupby 可能把内存直接打满。我一般直接换 DuckDB,把 CSV 当表查询,聚合计算在列式引擎里跑,中间结果不落地,内存占用比 DataFrame 低很多。
import duckdb duckdb.sql(""" CREATE OR REPLACE TABLE cdr AS SELECT start_time::TIMESTAMP AS ts, imei::VARCHAR AS imei, mcc::VARCHAR AS mcc, mnc::VARCHAR AS mnc, lac::VARCHAR AS lac, cell_id::VARCHAR AS cell_id, duration_sec::BIGINT AS duration_sec, call_type::VARCHAR AS call_type FROM read_csv_auto( 'cdr_raw.csv', header=true, timestampformat='%Y-%m-%d %H:%M:%S' ); COPY ( SELECT mcc, mnc, lac, cell_id, imei, CAST(ts AS DATE) AS stats_date, COUNT(*) AS call_count, SUM(duration_sec) AS duration_sum FROM cdr WHERE call_type IN ('MO_CALL', 'MT_CALL') GROUP BY mcc, mnc, lac, cell_id, imei, CAST(ts AS DATE) ) TO 'cdr_summ_imei_cell_info.csv' (HEADER, DELIMITER ','); """)read_csv_auto 会自动推断类型,但我仍然建议在 SELECT 里显式写 VARCHAR,避免把 IMEI 推断成 BIGINT。timestampformat 必须和文件里的时间格式一致,否则时间列全是 NULL,后面 CAST(ts AS DATE) 也会跟着出错。COPY 的目标字段顺序要和后面 MySQL 建表字段一一对应,不然后面 LOAD DATA 还要额外做列映射。如果单机还跑不动,就把原始文件按小时或按天切成多个文件,逐个聚合后 UNION ALL,但这套代码对几十 GB 级别已经够用。
3.3 压成 7z:为什么用 7z 而不是 zip
CSV 是纯文本,重复率高,7z 默认的 LZMA 压缩率通常比 zip 小 30% 到 50%。这也是为什么这类交付包习惯用 .7z 而不是 .zip 的原因。
# a 表示添加,-mx=9 为极限压缩,-mmt=on 开启多线程 7z a -mx=9 -mmt=on "cdr_summ_imei_cell_info(csv-mysql).7z" \ cdr_summ_imei_cell_info.csv schema.sql-mx=9 是最佳压缩比,速度比默认慢,但文本文件收益明显。-mmt=on 让压缩过程吃满多核,压缩大 CSV 时能省不少时间。文件名里的括号同样要用双引号包住,否则 shell 会把括号当成子进程语法。压完建议立刻执行7z t "cdr_summ_imei_cell_info(csv-mysql).7z"验证完整性,再往外交付。
4. 把汇总 CSV 搬进 MySQL:建表、LOAD DATA、跑批三步走
CSV 验证没问题后,下一步就是入库。为什么不逐个 INSERT?几百万行逐条插入,事务开销和 SQL 解析开销都很大。正确路径是先把表结构设计好,再用 LOAD DATA 批量导入,最后核对行数。这三步缺一不可。
4.1 表结构设计:主键怎么定、字符集选什么
建表是在导入之前必须确认的,不是让 MySQL 自己猜。下面的结构适合“设备 x 小区 x 日”粒度的摘要。
CREATE DATABASE IF NOT EXISTS cdr_analysis CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE cdr_analysis; CREATE TABLE IF NOT EXISTS cdr_summ_imei_cell_info ( stats_date DATE NOT NULL, mcc VARCHAR(3) NOT NULL, mnc VARCHAR(3) NOT NULL, lac VARCHAR(5) NOT NULL, cell_id VARCHAR(8) NOT NULL, imei VARCHAR(15) NOT NULL, call_count INT UNSIGNED NOT NULL DEFAULT 0, duration_sum BIGINT UNSIGNED NOT NULL DEFAULT 0, PRIMARY KEY (stats_date, mcc, mnc, lac, cell_id, imei), KEY idx_location (mcc, mnc, lac, cell_id), KEY idx_imei (imei) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;主键把 stats_date 放第一位,这对按日清理、按日分区非常友好。mcc、mnc、lac、cell_id 全部用 VARCHAR,不要用 INT,因为有些厂家的小区 ID 可能带十六进制前缀。imei 用 VARCHAR(15),不要用 BIGINT,原因下一章会专门说。字符集用 utf8mb4 而不是 utf8mb3,因为后者不支持部分生僻字符和四字节编码;排序规则 unicode_ci 足够日常使用。查询如果经常按小区聚合,idx_location 能帮上忙;如果经常查设备轨迹,idx_imei 是必须的。
4.2 LOAD DATA INFILE 才是快路:跳过表头、处理分隔符
建好表以后,导入用 LOAD DATA,这条语句是 MySQL 批量导入里最常用、也最快的方式。
SET NAMES utf8mb4; SET SESSION time_zone = '+08:00'; LOAD DATA LOCAL INFILE '/path/cdr_summ_imei_cell_info.csv' INTO TABLE cdr_summ_imei_cell_info CHARACTER SET utf8mb4 FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '"' LINES TERMINATED BY '\n' IGNORE 1 ROWS (stats_date, mcc, mnc, lac, cell_id, imei, call_count, duration_sum);LOAD DATA LOCAL 表示文件在客户端机器上,不是 MySQL 服务器本机。如果文件已经放在服务器上,可以去掉 LOCAL,但需要客户端账号有 FILE 权限。CHARACTER SET utf8mb4 要和建表字符集一致。FIELDS TERMINATED BY ',' 对应 CSV 的逗号分隔;OPTIONALLY ENCLOSED BY '"' 表示只有含特殊字符的字段会被双引号包住,没有也无所谓。IGNORE 1 ROWS 跳过表头。导入前先执行 SET NAMES 和 time_zone,能避免字符乱码和时间错位。
4.3 校验行数:导入前后的统计必须对齐
导入完不核对等于没导入。最简单的核对是 CSV 数据行数和 MySQL 表行数对账。CSV 行数用 Python 数最稳,避免字段内含换行导致 wc -l 不准确。
import csv with open("cdr_summ_imei_cell_info.csv", encoding="utf-8") as f: n = sum(1 for _ in csv.reader(f)) - 1 print("CSV 数据行:", n)mysql -uuser -p db -e \ "SELECT COUNT(*) FROM cdr_summ_imei_cell_info;"Python 的 csv.reader 会正确处理带引号的字段,就算某个字段里真的出现换行,它也不会多数一行。减 1 是去掉表头。MySQL 查询出的行数如果比 CSV 数据行数少,最常见的原因是 LOAD DATA 撞了主键被忽略;如果多,说明 CSV 本身有重复行。两种情况都要回到源数据排查,不要直接认为入库成功。
5. 从解包到入库的避坑记录:我把 CSV 导入 MySQL 时踩过的 4 个坑
下面这几条不是原理推测,是我在类似交付包里反复踩过的实际教训。每一条都按现象、原因、解决展开,也许能帮你省下一个通宵。
5.1 IMEI 变科学计数法:CSV 和 MySQL 都说主键不唯一
现象:CSV 里 IMEI 显示成 8.6028E+14,读取后附近几行值看起来一样,导入 MySQL 时报主键重复,查询出来的数据也是乱的。
原因:15 位纯数字在 Excel 和很多程序里默认按浮点数处理,double 精度只有 15 位有效数字,后几位被抹成 0。IMEI 一旦被当数字存,等于多条记录的主键前缀相同,必然冲突。
解决:生成 CSV 时强制把 imei 列输出为文本,pandas 里用 dtype=str,Excel 打开时不要重新保存为标准数值。MySQL 建表用 VARCHAR(15),不要用 BIGINT。如果已经导坏,先把 CSV 里的原始文本 ID 重新清洗后再导入,不要尝试 UPDATE 补,因为丢失的位数已经无法找回。
5.2 时区差 8 小时:统计日期整体错位
现象:CDR 原始时间戳是 UTC,CSV 里的 stats_date 却按本地日期生成,凌晨 0 点到 8 点之间的话单被归到前一天,日报数据每天看起来都偏小。
原因:解析时间戳时没有统一时区,pandas 拿到的是不带时区的 naive 时间;导入 MySQL 时,会话时区也不是东八区,导致 TIMESTAMP 类型的展示和存储再次偏移。
解决:第一道关在生成 CSV 时就处理,用带时区的时间转换:pd.to_datetime(raw["start_time"], utc=True).dt.tz_convert("Asia/Shanghai"),然后再取日期。第二道关是导入前执行SET SESSION time_zone = '+08:00',保证会话内时间一致。最省事的是在包内 README 里写明所有时间均为东八区,让下游不用猜。
5.3 LOAD DATA 撞主键:同日增量导入翻车
现象:第一天跑批成功,第二天重跑同一天数据,LOAD DATA 报 Duplicate entry,整批导入失败,日志里全是主键冲突。
原因:主键包含 stats_date,但增量导入时没有先清理同一天的旧数据。LOAD DATA 默认遇到主键冲突会停止,除非显式声明 IGNORE。
解决:按日期先删后插,导入前执行:
DELETE FROM cdr_summ_imei_cell_info WHERE stats_date = '2025-01-01';然后再 LOAD DATA。如果每天的包实际是小时级追加,不适合整日删除,就用临时表加 INSERT ... ON DUPLICATE KEY UPDATE 合并,具体写法在下一章。
5.4 括号文件名把脚本坑了:解压后再看不到 CSV
现象:脚本里写的是7z x cdr_summ_imei_cell_info(csv-mysql).7z,运行报错,或者解压出来的文件不在预期目录,后续 Python 找不到 CSV。
原因:shell 把括号解释成了子 shell 语法,文件名被拆成两段,7z 收到的参数根本不是完整文件路径。
解决:始终用双引号包住整个文件名:
7z x "cdr_summ_imei_cell_info(csv-mysql).7z" -o/tmp/cdr_pkg用 Python 调用 subprocess 时也不要拼字符串,直接传参数列表。解压后立刻 ls 目标目录确认路径,再继续后续处理。这个小问题看似低级,但在自动化脚本里最容易反复出现。
6. 进阶用法:给 7z 包加校验和,让它变成可追溯的历史快照
当这套流程跑了一周之后,你会遇到一个新问题:每天产出好几个 7z 包,时间久了根本分不清哪个文件在传输过程中坏了。我后来给每个交付包都随身带两个文件:manifest.txt 和 checksums.sha256。
6.1 用 SHA256 校验 CSV 和 SQL 文件
生成校验和要在打包之前做,这样下游解压后可以直接核对解压出来的文件,不用重新解压整个包。
# 对要打包的文件生成校验和 sha256sum cdr_summ_imei_cell_info.csv schema.sql > checksums.sha256 # 把校验和一起压进包 7z a "cdr_summ_imei_cell_info(csv-mysql).7z" \ cdr_summ_imei_cell_info.csv schema.sql checksums.sha256 # 解压后核对 sha256sum -c checksums.sha256sha256sum -c 会逐个核对文件,输出 OK 表示完整,输出 FAILED 表示文件已损坏。配合7z t做包级校验,能快速判断是压缩壳坏了还是包内文件坏了。这是数据交付的后悔药,成本极低,收益很高。
6.2 同一天数据重复到达时:用临时表合并而不是覆盖
如果业务每天不是全量重算,而是每小时追加一批,DELETE 再 LOAD 会把先到的小时段数据丢掉。我一般用临时表加 ON DUPLICATE KEY UPDATE 合并:
CREATE TABLE tmp_cdr LIKE cdr_summ_imei_cell_info; LOAD DATA LOCAL INFILE '/path/cdr_summ_imei_cell_info.csv' INTO TABLE tmp_cdr CHARACTER SET utf8mb4 FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '"' LINES TERMINATED BY '\n' IGNORE 1 ROWS (stats_date, mcc, mnc, lac, cell_id, imei, call_count, duration_sum); INSERT INTO cdr_summ_imei_cell_info SELECT * FROM tmp_cdr AS new ON DUPLICATE KEY UPDATE call_count = new.call_count, duration_sum = new.duration_sum; DROP TABLE tmp_cdr;临时表和目标表结构一致,LOAD DATA 照常跑,最后 INSERT SELECT 只处理主键冲突的行。写入时如果临时表里同一主键出现多行,最后一行生效;所以临时表也建议和正式表一样建主键,或者导入前先按主键去重。这个写法让重复到货的数据包变得安全,不会像 DELETE 覆盖那样造成统计缺口。
我最开始做 CDR 交付时偷懒,省略了校验和,结果有一次包传了三手,下游导完发现 SQL 少了一张表,查了很久才发现是压缩包本身坏了。那之后我养成一个习惯:每个 7z 包先算 SHA256 再压缩,压缩完再 7z t 一次。这个习惯不复杂,但能让你在半夜被人叫醒时,用一句话证明是包坏了,而不是数据算错。希望帮到你。
本文还有配套的精品资源,点击获取