numbers days and time

本文详细介绍了英语中的数字表达方式,包括基数词、序数词、分数、小数、百分数等的书写规则及其应用场景,同时还涉及了时间、日期、数学运算等特定场景下的数字表达。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

英语数字表达方式汇总

在【牛津高阶英汉双解词典】的附录中有详尽介绍。在此,仅做网络上搜集内容的整理。

数 词:主要分为基数词和序数词两类。

一、 数词的分类

 4-只有40相关的写法为forty9类只有第九写作ninth

 1. 基数词   表示数目的词称为基数词。其形式如下:

  A.从1——10

      onetwothreefourfivesixseveneightnineten

  B.从 11——19

      eleventwelve thirteen fourteen fifteen sixteen seventeeneighteen nineteen

    这里除 eleven twelve thirteen fifteen eighteen为特殊形式外,fourteensixteenseventeennineteen都是由其个位数形式后添加后缀-teen构成。

  C.从 21——99

整数几十中除twentythirty, fortyfiftyeighty为特殊形式外,sixtyseventyninety都是其个位数形式后添加后缀-ty构成。表示几十几时,在几十和个位基数词形式之间添加连字符“-

     21 twenty-one

     76 seventy-six

  D.百位数

     个数基数词形式加“hundred”,表示几百,在几十几与百位间加上and

      101 a hundred and one

      320 three hundred and twenty

      648 six hundred and forty-eight

  E.千位数以上

  从数字的右端向左端数起,每三位数加一个逗号“,”。从右开始,第一个“,”前的数字后添加 thousand,第二个“,”前面的数字后添加 million,第三个“,”前的数字后添加 billion。然后一节一节分别表示,两个逗号之间最大的数为百位数形式。【and 读作【n】 】

  2,648 two thousand six hundred and forty-eight 

  16,250,064 sixteen million two hundred and fifty thousand sixty-four

  5,237,166,234 five billiontwo hundred and thirty-seven millionone hundred and sixty-six thousandtwo hundred and thirty-four

  F.基数词在表示确切的数字时,不能使用百、千、百万、十亿的复数形式;但是,当基数词表示不确切数字,如成百、成千上万,三三两两时,基数词则以复数形式出现。

  There are hundreds of people in the hall

  大厅里有数以百计的人。

  Thousands and thousands of people come to visit the Museum of Qin Terra-Cotta Warriors and Horses every day

  每天有成千上万的人来参观秦兵马涌博物馆。

  They went to the theatre in twos and threes

  他们三三两两地来到了剧院。

  G.表示人的不确切岁数或年代,用几十的复数形式表示。

  He became a professor in his thirties

  他三十多岁时成为了教授。

  She died of lung cancer in forties

  她四十来岁时死于肺癌。

  It was in the 1960s

  那是在二十世纪六十年代。

  H.基数词的句法功能

  基数词在句中可作主语、宾语、定语、表语、同位语。

  The two happily opened the box

  两个人高兴地打开了盒子。(作主语)

  I need three altogether

  我总共需要三个。(作宾语)

  Four students are playing volleyball outside

  四个学生在外面打排球。(作定语)

  We are sixteen

  我们是16个人。(作表语)

  They three tried to finish the task before sunset

  他们三个人尽力想在日落前完成任务。(作同位语)

 2. 序数词 (牢记基本,其他只尾巴变基本)

   表示顺序的词称为序数词。序数词的主要形式:

  A.从第一至第十九

  其中,one first two second three third five fiftheighteighthnineninthtwelve twelfth为特殊形式,其它的序数词都是由其相对应的基数词后面添加“th”构成。例如: six sixthnineteen nineteenth

  B.从第二十至第九十九

  整数第几十的形式由其对应的基数词改变结尾字母yi,再加“eth”构成。

  twenty——twentieth thirty——thirtieth

  表示第几十几时,用几十的基数词形式加上连字符“-”和个位序数词形式一起表示。

  thirty-first 第三十一

  fifty-sixth 第五十六

  seventy-third 第七十三

  ninety-ninth 第九十九

  C.第一百以上的多位序数词

  由基数词的形式变结尾部分为序数词形式来表示。

  one hundred and twenty-first 第一百二十一

  one thousandthree hundred and twentieth 第一千三百二十

  D.序数词的缩写形式

  有时,序数词可以用缩写形式来表示。主要缩写形式有。

  first——lst second——2nd third——3rd

  fourth——4th sixth——6th twentieth——20th

  twenty-third——23rd

  其中lst2nd3rd为特殊形式,其它的都是阿拉伯数字后加上th

  E.序数词的句法功能

  序数词在句中可作主语、宾语、定语和表语。

  The second is what I really need

  第二个是我真正需要的。(作主语)

  He choose the second

  他挑选了第二个。(作宾语)

  We are to carry out the first plan

  我们将执行第一个计划。(作定语)

  She is the second in our class.在我们班她是第二名。(作表语)

注:序数词在使用时,通常前面要加定冠词 the;但是如果序数词前出现不定冠词aan时,则表示“再——”,“又——”。

  We/'ll go over it a second time

  我们得再念第二遍。

  We/'ve tried it three timesMust we try it a fourth time?

  我们已经试过三遍了,还必须试一次(第四次)吗?

  另外,基数词也可以表示顺序。只需将基数词放在它所修饰的名词之后即可,不需要添加定冠词。

  the first lesson——Lesson One

  the fifth page——Page 5five

  the twenty-first room——Room 21twenty-one

 

二、时刻表示法

(整点-直读;过点-past;差点-to;日常-时分各为整体读)

 1. 表示几点钟用基数词加可以省略的o/'clock

  5:00 读作 five o/'clock five

 2. 表示几点过几分,在分钟后加past,再加小时

  five past seven 七点过五分

  half past six 六点半

  a quarter past eight 八点过一刻

  seven past eight 八点过七分  3. 表示几点差几分,在分钟后面加to,再加小时

  ten to eight 差十分八点(七点五十分)

  a quarter to twelve 差一刻十二点(十一点四十五分)

  twenty to six 差二十分六点(五点四十分)

 在日常生活中,常用下列简单方法表示时间。

 以小时、分种为单位分别读出数字。

  6:31   读作 six thirty-one

  10:26  读作 ten twenty-six

  14:03 读作 fourteen o three

  16:15 读作 sixteen fifteen

  18:30 读作 eighteen thirty

  23:55 读作 twenty-three fifty-five

 注:时刻表上的时间大多采用24小时表示法,这样就不需要用a.m.表示上午,p.m.表示下午了。

 

三、年月表示法

 1. 世纪可以用定冠词加序数词加世纪century表示,也可以用定冠词加百位进数加/'s表示

  the sixth6thcentury 公元六世纪

  the eighteenth18thcentury 公元十八世纪

  the 1900/'s 二十世纪

  the 1600/'s 十七世纪

  这里,用百位数整数表示的世纪比这一百位阿拉伯数字本身多一个世纪。

 2. 年代用定冠词及基数词表示的世纪加十位整数的复数形式构成

  in the 1930/'sin the thirties of the twentieth century in the nineteen thirties

  在二十世纪三十年代

  in the 1860/'sin the sixties of the 19th century in the eighteen sixties

  在十九世纪六十年代

  In the 1870/'s when Marx was already in his fiftieshe found it important to study the situation in Russiaso he began to learn Russian

  在十九世纪七十年代当马克思已经五十多岁时,他发现研究俄国的形势很重要,便开始学习俄语。

 3. 表示某年代的早期、中期和晚期,可以在定冠词后,年代前添加 earlymid-late

  in the early 1920/'s 在二十世纪二十年代早期

  in the mid-1950/'s 在二十世纪五十年代中期

 4. 年月日表示法

(年:千为单位、百为单位、十为单位,取决于组合方式)

(月:in + 大写首字母)

(日:on + the + 序数词)

  A.年份用基数词表示,一般写为阿拉伯数字,读时可以以hundred为单位,也可以以世纪、年代为单位分别来读。

  1949 读作 nineteen hundred and forty-nine nineteen forty-nine

  1800 读作 eighteen hundred

  253 读作 two hundred and fifty-threetwo fifty-three

  1902 读作 nineteen hundred and two nineteen o two

  表示在哪一年,一般在年数前加介词in,使用year时,year放在数词之前。

  in the year two fifty-three B.C. 在公元前253

  但是,通常采用in加表示年份的阿拉伯数字。

  B. 月份,在哪个月用介词in加第一个字母大写的月份词表示。例如:in May在五月; in July在七月。为了简便起见,月份与日期连用时,月份常用缩写形式表示。缩写形式除MayJuneJuly外,其它的月份都由其前三个字母表示,但September除外。

  January——Jan.一月 February——Feb.二月   March——Mar. 三月 April——Apr.四月

  August——Aug.八月   September——Sept.九月   October——Oct.十月

  November——Nov.十一月   December——Dec.十二月

  注:这里缩写形式后面加点不能省略,因为它是表示缩写形式的符号。

  C.日期用定冠词the加序数词表示。在哪一天要添加介词on

  National Day is on Oct. 1.

  国庆节是十月一日。(读作 October first

  此句也可以表示为 National Day is on the 1st of October

  May 5 五月五日(读作May fifth

  也可以表示为the fifth5thof May

  Mar. 1st) 三月一日(读作March first the first of March

 5. 表示不具体、不确切的时间,如世纪、年代、年份、月份时,用介词in表示,表示具体确切的某一天用介词on表示通常情况下morningafternoonevening等词前用介词in。但是,当这些词前后有一修饰限定的词作为定语,把它们限定为某一天早晨、下午或晚上时,介词in应改为on。这里的修饰限定词可以是介词短语、名词、形容词、定语从句等。

  On a cold morningthe old man died lonely in his house

  在一个寒冷的早晨,这个老人孤独地死在自己的房子里。

  I don/'t want to be disturbed on nights when I have to study

  我不愿意在我必须学习的晚上被打扰。

  The accident happened on the afternoon of July 7

  这次事故发生在77日下午。

  We are to have a small test on Monday morning

  星期一早晨我们将进行一次小测验。   

英文日期

中国:年月日

美国:月日年

英国:日月年

美式表达:
Aug. 13, 2008                       Aug. 13th , 2008                   8/13/2008
英式表达:
13  Aug ,  2008                 13th  Aug , 2008                 13/8/2008
以上8月都是缩写,在正式场合要用全拼August.

公元:

公元常以 A.D.(拉丁文 Anno Domini 的缩写,意为主的生年)表示,公元前则以 B.C.(英文 Before Christ 的缩写,意为基督以前)表示。

月份:

一月 January                          二月 February                    三月 March

四月 April                               五月 May                            六月 June

七月 July                              八月 august                          九月 September

十月 October                        十一月November                  十二月December

星期:

星期日 Sunday                     星期一 Monday                    星期二 Tuesday

星期三 Wednesday              星期四 Thursday                  星期五 Friday

星期六 Saturday

月份的缩写:

1 January Jan 2 February Feb 3 March Mar. 4 April Apr. 5 May May 6 June Jun. 7 July Jul. 8 August Aug. 9 September Sept. 10 October Oct. 11 November Nov. 12 December Dec. May 是没有缩写的.其他的如上.缩写第一个字母一定要大写,后面得有个点. 其中九月是4个字母的哦.Sept

星期的缩写:
 
 Sunday(Sun.)Monday(Mon.)Tuesday(Tues.)Wednesday(Wed.)Thursday(Thur. or Thurs.)Friday (Fri.)Saturday (Sat.)
  记忆方法:除星期二取前四个字母和星期四取前四个或五个字母外,其余单词均取前三个字母,并在右下方加上小圆点

年代读法
1.
年代   前用in
   
大多数的没有“0”的年代都是看作两个十位的数字来说
           
比如:1981 nineteen eighty-one
但是有零的就不一样了:

1)三个零:就是把年代直接读作 ...thousand
    
如:2000 two thousand

2)两个零在中间:要读作:... thousand and ...
     
如:1009 one thousand and nine
              2008 two thousand and eight

3)两个零在结尾:要把前面两个数字读成十位数,后面加上hundred
    
如:1900 nineteen hundred

4)一个零在百位,一个零在个位:读作:...thousand and ...ty
    
如:1090 one thousand and ninety

5
)只有一个零在个位:仍旧看成两个十位数来读
     
如:1990 nineteen ninety

6)只有一个零在十位:前面两位当作十位数来     读,十位的零读作“oh”,个位直接读
      
如:1906 nineteen oh six

7)只有一个零在百位:有两种读法:
    
第一种读法:仍旧看成两个十位数来读

        如:2015 twenty fifteen
    
第二种读法:读作 ... thousand and ...
       
如:2015 two thousand and fifteen

8)不足四位的年代,则直接读作数字
       
如:605      AD six oh five

标点:在月、日和年之间或星期和月、日和年之间要加逗号。

举例ThursdayFebruary 52004

 

 

四、加减乘除表示法

 1. “加”用plusandadd表示;“等于”用ismakeequal等词表示。

  2+3=? 可表示为: How much is two plus three?

  2+3=5

  Two plus three is five

  Two and three is equal to five

  Two and three make five

  Two added to three equals five

  If we add two to/and three, we get five

  二加三等于五

 2. “减”用 minus take from表示

  106? How much is ten minus six?

  10-6=4  Ten minus six is four

  Take six from ten and the remainder is four.

  Six (taken) from ten is four

  十减去六等于四

 3. “乘”用time(动词)或multiply表示

  3X4? How much is three times four?

  3X4=12 Three times four is/are twelve

  Multiply three by fourwe get twelve

  Three multiplied by four makes twelve

  三乘以四等于十二

 4. “除”用divide的过去分词形式表示

  16÷4=? How much is sixteen divided by four?

  16÷44 Sixteen divided by four is four

  Sixteen divided by four equals/gives/makes four

  十六除以四等于四。

 

五、分数表示法

 1. 分数是由基数词和序数词一起来表示的。基数词作分子,序数词作分母,除了分子是“1”以外,其它情况下序数词都要用复数形式。

  3/4 three fourths three quarters

  1/3 one thirda third

  24/25 twenty-four twenty-fifths

  3 1/4 three and one fourth three and one quarter

  1/2 a half

  1/4 one quartera quarter

  1 1/2 one and a half

  1 1/4 one and a quarter

 2. 当分数后面接名词时,如果分数表示的值大于1,名词用复数;小于1,名词用单数。

  1 1/2 hours 一个半小时(读作 one and a half hours

  2 3/4 meters 二又四分之三米(读作two and three-fourths meters

  4/5 meter 五分之四米

  5/6 inch 六分之五英寸

 3. 表示“n次方”的说法:指数用序数词,底数用基数词。

  107次方 the seventh power of tenten to the seventh power

  610次方 the tenth power of sixsix to the tenth power

 

六 、小数表示法

 1. 小数用基数词来表示,以小数点为界,小数点左首的数字为一个单位,表示整数,数字合起来读;小数点右首的数字为一个单位,表示小数,数字分开来读;小数点读作 pointo读作 zerooou],整数部分为零时,可以省略不读。

  0.4 zero point fourpoint four 零点四

  10.23 ten point two three 十点二三

  25.67 twenty-five point six seven 二十五点六七

  l.03 one point o three 一点零三

 2. 当数字值大于1时,小数后面的名词用复数,数字值小于1时,小数后面的名词用单数。

  1.03 meters 一点零三米 0.49 ton 零点四九吨

  l.5 tons 一点五吨

 

七、百分数表示法

  百分数用基数+percent表示

  50 fifty percent   百分之五十

  3 three percent   百分之三

  0.12 zero point one two percent 百分之零点一二

  这里的percent前半部per表示“每一”,cent这一后半部分表示“百”,所以百分之几中percent不用复数形式。

 

八、数量表示法

 1. 表示长、宽、高、面积等,用基数词+单位词(meterfootinchkilogram等)+ 形容词(longwidehigh等)表示,或者用基数词+单位词 + in + 名词(length width height weight等)表示。

  two meters long two meters in length 2

  three feet high three feet in height 3英尺

  four inches wide four inches in width 4英寸

  This box is 2 kilograms in weight  这个盒子有两千克重。

  The city wall of Xi/'an is 12 meters wide and 12 meters high  西安城墙是12米宽,12米高。

 2. 表示时间、距离时,使用含数词的名词所有格形式作定语。

  five minutes/' walk   步行五分钟(的距离)

  It/'s an hour/'s ride from my hometown to our university

  从我的家乡到我们大学是乘车一小时的路程。

  或:从我的家乡到我们大学需要乘车一小时。

  It/'s three kilometers/' distance from our campus to the Bell Tower.

  从我们校园到钟楼有三公里远。

 3. 表示温度时,用below zero表示零下温度,温度用基数词+degrees+单位词(centigrade摄氏或Fahrenheit华氏)表示。

  thirty-six degrees centigrade 36 摄氏 36

  four degrees below zero centigrade -4 摄氏零下4

  Water freezes at thirty-two degrees Fahrenheit  水在华氏三十二度时结冰。

  Water boils at one hundred degrees centigrade  水在摄氏一百度时沸腾。

  这里的单位词在人们都很清楚是什么度量制度时,可以省略。

  You are 37.(读作 thirty-seven degrees  你是三十七度。(摄氏)

  It/'s seven degrees below zero  今天是零下七度。(摄氏)

 4. 由数词和其他名词构成的名词性短语作定语时,其中的名词用单数形式,名词性短语中各部分间要用连字符“-”来连接。

  It/'s a five-minute walk from the library to the playground  从图书馆到操场需要走五分钟。

  She/'s a sixteen-year-old girl  她是个十六岁的女孩。

 5. 表示“比···大(或)几倍”的说法。

  This room is two times bigger than that one  这个房间比那个(房间)大两倍。

  The dictionary is four times thicker than that book  这本词典比那本书厚四倍。

  My age is two times older than his  我的年龄比他大两倍。

 

import pandas as pd import logging from geopy.distance import geodesic import time from contextlib import contextmanager import numpy as np from sqlalchemy import create_engine, text from tenacity import retry, stop_after_attempt, wait_exponential from datetime import datetime # -------------------------- # 通用配置 # -------------------------- DB_CONFIG = { "conn_str": "mysql+pymysql://root:root@127.0.0.1:3306/test?charset=utf8", "pool_size": 10, "pool_recycle": 300 } member_list_path = r"D:\OneDrive\ERIC\维护\平台数据\代维信息\2025-07人员信息快照_20250708.xlsx" member_list_columns = ['登陆账号', '所属组织', '所属地市', '在职状态'] TIME_RANGE = { "start": '2025-07-01 00:00:00', "end": '2025-07-08 23:59:59' } # 定义日期格式转换器 def format_time_range(time_range): # 解析开始时间和结束时间 start_date = datetime.strptime(time_range["start"], '%Y-%m-%d %H:%M:%S') end_date = datetime.strptime(time_range["end"], '%Y-%m-%d %H:%M:%S') # 格式化为YYYYMMDD形式并拼接 return f"{start_date.strftime('%Y%m%d')}~{end_date.strftime('%Y%m%d')}" # 获取格式化后的时间范围 source_time = format_time_range(TIME_RANGE) FILE_PATHS = { "cran_info": r'D:\OneDrive\ERIC\维护\test\CRAN机房信息.csv', "output": fr'D:\OneDrive\ERIC\维护\工单\现场执行工单\现场执行工单_{source_time}.xlsx' } TABLE_CONFIG = { '工单_保障': {'columns': ["工单当前状态", "工单号", "工单分类", "维护分类"]}, '工单_巡检': {'columns': [ "工单当前状态", "工单号", "资源ID", "资源名称", "资源经度", "资源纬度", "STATION_NAME", "STATION_ID", "STATION_LEVEL", "LONGITUDE", "LATITUDE", "工单分类", "维护分类" ]}, '工单_拆站': {'columns': [ "工单当前状态", "工单号", "资源cid AS 资源ID", "资源名称", "资源经度", "资源纬度", "STATION_NAME", "STATION_ID", "STATION_LEVEL", "LONGITUDE", "LATITUDE", "工单分类", "维护分类" ]}, '工单_验收': {'columns': [ "工单当前状态", "工单号", "资源cid AS 资源ID", "资源名称", "资源经度", "资源纬度", "STATION_NAME", "STATION_ID", "STATION_LEVEL", "LONGITUDE", "LATITUDE", "工单分类", "维护分类" ]}, '工单_发电': {'columns': [ "工单当前状态", "工单号", "站点ID AS 资源ID", "站点名称 AS 资源名称", "站点经度 AS 资源经度", "站点纬度 AS 资源纬度", "STATION_NAME", "STATION_ID", "STATION_LEVEL", "LONGITUDE", "LATITUDE", "工单分类", "维护分类" ]}, '工单_通用': {'columns': [ "工单当前状态", "工单号", "资源名称", "资源经度", "资源纬度", "STATION_NAME", "STATION_ID", "STATION_LEVEL", "LONGITUDE", "LATITUDE", "工单分类", "维护分类" ]}, '故障工单': {'columns': [ "工单状态 AS 工单当前状态", "工单编号 AS 工单号", "STATION_NAME AS 资源名称", "LONGITUDE AS 资源经度", "LATITUDE AS 资源纬度", "STATION_NAME", "STATION_ID", "STATION_LEVEL", "LONGITUDE", "LATITUDE", "工单分类", "维护分类", "故障处理时长_小时", "是否延期有效", "是否及时处理", "高频故障", "是否超长退服", "网元分类" ], 'where_column': '工单编号'} } DIMENSIONS = [ {'name': '执行人', 'keys': ['执行人']}, {'name': '所属组织', 'keys': ['所属组织']}, {'name': '地市', 'keys': ['地市']}, {'name': '代维简称', 'keys': ['代维简称']}, {'name': '地市代维', 'keys': ['地市', '代维简称']} ] # -------------------------- # 工具函数 # -------------------------- @contextmanager def time_monitor(step_name): """耗时监控上下文管理器""" start_time = time.time() try: yield finally: print(f"{step_name} 耗时: {time.time() - start_time:.4f}秒") def setup_logging(): """日志配置""" logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s', handlers=[ logging.FileHandler("data_processor_v2.log"), logging.StreamHandler() ] ) return logging.getLogger(__name__) logger = setup_logging() @retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=4, max=10)) def safe_read_sql(sql: text, engine, params: dict = None) -> pd.DataFrame: """带重试机制的SQL读取""" logger.debug(f"执行SQL: {sql} \n参数: {params}") return pd.read_sql(sql, engine, params=params) def init_db_engine(): """初始化数据库引擎""" return create_engine( DB_CONFIG["conn_str"], pool_size=DB_CONFIG["pool_size"], pool_recycle=DB_CONFIG["pool_recycle"] ) # -------------------------- # 数据校验函数 # -------------------------- def is_valid_coordinates(lat, lon): """经纬度有效性校验""" if pd.isna(lat) or pd.isna(lon): return False if not (-90 <= lat <= 90 and -180 <= lon <= 180): return False if abs(lat) < 1e-6 and abs(lon) < 1e-6: return False return True def calculate_distance(coord1, coord2): """带校验的距离计算""" try: lat1, lon1 = coord1 lat2, lon2 = coord2 except (TypeError, ValueError) as e: logger.debug(f"坐标解析失败: {str(e)} | coord1={coord1} coord2={coord2}") return 99999.0 if not (is_valid_coordinates(lat1, lon1) and is_valid_coordinates(lat2, lon2)): return 99999.0 return geodesic(coord1, coord2).meters # -------------------------- # 数据加载模块 # -------------------------- def load_site_orders(engine): """加载现场执行工单数据""" sql = text(""" SELECT 地市, 代维简称, 专业, 工单类型, 工单编号, 执行人, 所属组织, 出发时间, 出发经度, 出发纬度, 签到时间, 签到经度, 签到纬度, 完成时间, 完成经度, 完成纬度, 路途时长, 实际工作时长 FROM 现场执行工单 WHERE 出发时间 BETWEEN :start_time AND :end_time """) params = { 'start_time': TIME_RANGE["start"], 'end_time': TIME_RANGE["end"] } df = safe_read_sql(sql, engine, params) df = df[~df['所属组织'].str.contains('高铁')] df['代维简称'] = df['代维简称'].replace('中移铁通', '铁通') valid_companies = ['中贝', '中通服', '中邮建', '唐人', '宜通', '怡创', '浙通服', '润建', '虹信', '超讯', '铁通', '长实'] df = df[df['代维简称'].isin(valid_companies)] df['地市代维'] = df['地市'].str.split("-").str[0] + '-' + df['代维简称'] logger.info(f"加载现场工单记录数: {len(df)}") return df def batch_load_order_data(engine, order_numbers, batch_size=500): """批量加载工单相关数据""" dfs = [] for table, config in TABLE_CONFIG.items(): try: columns = ", ".join(config['columns']) where_col = config.get('where_column', '工单号') # 修复:正确构造SQL查询字符串,确保引号闭合 sql_template = text(f"SELECT {columns} FROM `{table}` WHERE `{where_col}` IN :order_nums") table_dfs = [] for i in range(0, len(order_numbers), batch_size): batch = order_numbers[i:i + batch_size] df = safe_read_sql(sql_template, engine, params={'order_nums': batch}) table_dfs.append(df) if table_dfs: table_df = pd.concat(table_dfs, ignore_index=True) dfs.append(table_df) logger.info(f"表 {table} 加载完成,记录数: {len(table_df)}") except Exception as e: logger.error(f"加载表 {table} 失败: {str(e)}", exc_info=True) return dfs # -------------------------- # 数据处理模块 # -------------------------- def merge_order_data(site_df, order_dfs): """合并工单数据(增强版)""" if not order_dfs: logger.warning("没有需要合并的工单数据") return pd.DataFrame() final_df = pd.concat(order_dfs, axis=0, ignore_index=True) final_df = final_df.drop_duplicates(subset=['工单号'], keep='first') merged_df = pd.merge( left=site_df, right=final_df, left_on='工单编号', right_on='工单号', how='left', ) merged_df.drop(columns=['工单号'], inplace=True) merged_df['路途时长(分钟)'] = (merged_df['路途时长'] * 60).round(2) merged_df['实际工作时长(分钟)'] = (merged_df['实际工作时长'] * 60).round(2) # 过滤有效状态 condition = ( merged_df['工单当前状态'].notna() & (merged_df['工单当前状态'] != '') & ~merged_df['工单当前状态'].str.contains('已驳回|已撤销|已关闭', na=False) ) return merged_df[condition] def get_custom_natural_week(date): """ 计算“自然周”编号。本定义中,一周从上周三开始,到本周二结束。 """ if pd.isnull(date): return np.nan weekday = date.weekday() days_since_last_thursday = (weekday + 5) % 7 date = date - pd.Timedelta(days=days_since_last_thursday) return date.isocalendar()[1] # -------------------------- # 核心处理模块 # -------------------------- def generate_time_features(df): """增强时间特征生成""" df['出发时间'] = pd.to_datetime(df['出发时间'], errors='coerce') df['自然年'] = df['出发时间'].dt.year df['自然月'] = df['出发时间'].dt.to_period('M').astype(str) df['自然周'] = df['出发时间'].apply(get_custom_natural_week) df['出发日期'] = df['出发时间'].dt.date return df def process_coordinates(df): """坐标处理""" coord_columns = [ "资源纬度", "资源经度", "签到纬度", "签到经度", "LATITUDE", "LONGITUDE", "完成纬度", "完成经度" ] for col in coord_columns: if col in df.columns: df[col] = pd.to_numeric(df[col], errors='coerce') else: logger.warning(f"列 {col} 不存在") fault_columns = ['是否超长退服', '高频故障', '是否延期有效', '是否及时处理'] for col in fault_columns: if col not in df.columns: df[col] = '否' return df def calculate_distances(df): """距离计算""" df["签到距离"] = df.apply( lambda row: calculate_distance( (row["签到纬度"], row["签到经度"]), (row["资源纬度"], row["资源经度"]) ), axis=1 ) return df def merge_cran_data(main_df, cran_path): """合并CRAN机房数据并取最近RU距离""" cran_df = pd.read_csv(cran_path) for col in ["ru纬度", "ru经度"]: cran_df[col] = pd.to_numeric(cran_df[col], errors="coerce").fillna(0) merged = pd.merge( main_df[["工单编号", "STATION_NAME", "签到经度", "签到纬度"]], cran_df, left_on="STATION_NAME", right_on="station_name", how="left" ) merged["签到距离_CRAN"] = merged.apply( lambda row: calculate_distance( (row["签到纬度"], row["签到经度"]), (row["ru纬度"], row["ru经度"]) ), axis=1 ) min_distances = merged.groupby("工单编号", as_index=False)["签到距离_CRAN"].min() return pd.merge( main_df, min_distances, on="工单编号", how="left" ).fillna({"签到距离_CRAN": 99999.0}) # -------------------------- # 统计计算模块 # -------------------------- def calculate_workload(df): """工作量评定""" df['工作量评定'] = '有效' df['原因'] = '' no_completion = df['完成时间'].isna() df.loc[no_completion, ['工作量评定', '原因']] = ['无效', '无完成时间'] short_work = df['实际工作时长(分钟)'] < 5 df.loc[short_work, ['工作量评定', '原因']] = ['无效', '工作时长过短'] if all(col in df.columns for col in ['签到距离', '签到距离_CRAN', '维护分类']): invalid_dist = ( ~df['维护分类'].isin(['发电', '保障']) & (df['签到距离'] > 300) & (df['签到距离_CRAN'] > 300) ) df.loc[invalid_dist, ['工作量评定', '原因']] = ['无效', '签到距离过大'] return df def _calculate_base_stats(df, group_keys): """通用基础指标计算""" categories = df['维护分类'].dropna().unique() agg_dict = { # 总工单数(已去重) "总工单数": ('工单编号', 'nunique'), # 有效工单数(去重) "有效工单数": ('工单编号', lambda x: df.loc[x.index, '工单编号'][df['工作量评定'] == "有效"].nunique()), # 故障工单数(去重):统计工单分类为"故障工单"的工单编号唯一数 "故障工单数": ('工单编号', lambda x: df.loc[x.index, '工单编号'][df['工单分类'] == "故障工单"].nunique()), # 故障及时数(去重):统计同时满足工单分类为故障工单且是否及时处理为"是"的工单编号唯一数 "故障及时数": ('工单编号', lambda x: df.loc[x.index, '工单编号'][ (df['工单分类'] == "故障工单") & (df['是否及时处理'] == "是") ].nunique()), # 故障超时数(去重):统计同时满足工单分类为故障工单且是否及时处理为"否"的工单编号唯一数 "故障超时数": ('工单编号', lambda x: df.loc[x.index, '工单编号'][ (df['工单分类'] == "故障工单") & (df['是否及时处理'] == "否") ].nunique()), # 超长退服工单数(去重):统计是否超长退服为"是"的工单编号唯一数 "超长退服工单数": ('工单编号', lambda x: df.loc[x.index, '工单编号'][df['是否超长退服'] == "是"].nunique()), # 超频故障工单数(去重):统计高频故障为"是"的工单编号唯一数 "超频故障工单数": ('工单编号', lambda x: df.loc[x.index, '工单编号'][df['高频故障'] == "是"].nunique()), # 延期工单数(去重):统计是否延期有效为"是"的工单编号唯一数 "延期工单数": ('工单编号', lambda x: df.loc[x.index, '工单编号'][df['是否延期有效'] == "是"].nunique()), # 其他非去重指标保持不变 "故障处理时长": ('故障处理时长_小时', lambda x: x[df['工单分类'] == "故障工单"].sum()), "路途总时长": ('路途时长(分钟)', 'sum'), "工作总时长": ('实际工作时长(分钟)', 'sum'), "出勤人数": ('执行人', 'nunique'), "出勤人天": ('工单编号', lambda x: df.loc[x.index][['执行人', '出发日期']].drop_duplicates().shape[0]), } for cat in categories: agg_dict[f"{cat}工单数"] = ( '维护分类', lambda x, current_cat=cat: x.eq(current_cat).sum() ) stats = df.groupby(group_keys).agg(**agg_dict).reset_index() return stats def _add_derived_metrics(stats, time_level): """通用衍生指标计算""" stats['出勤总时长'] = stats['路途总时长'] + stats['工作总时长'] hour_cols = ['出勤总时长', '路途总时长', '工作总时长'] for col in hour_cols: stats[f'{col}(h)'] = (stats[col] / 60).round(2) stats['平均工单工作时长(h)'] = ( stats['工作总时长(h)'] / stats['总工单数'] ).replace([np.inf, -np.inf], 0).fillna(0).round(2) stats['平均工单路途时长(h)'] = ( stats['路途总时长(h)'] / stats['总工单数'] ).replace([np.inf, -np.inf], 0).fillna(0).round(2) if time_level == 'weekly': stats['人均能效_周'] = (stats['有效工单数'] / stats['出勤人天']).round(2) elif time_level == 'monthly': stats['人均能效_月'] = (stats['有效工单数'] / stats['出勤人天']).round(2) stats['故障处理及时率'] = (stats['故障及时数'] / stats['故障工单数']).replace(np.inf, 0) stats['工作时长占比'] = (stats['工作总时长'] / stats['出勤总时长']).replace(np.inf, 0) stats['人均出勤天数'] = (stats['出勤人天'] / stats['出勤人数']).replace(np.inf, 0).round(2) stats['日均出勤人数'] = (stats['出勤人天'] / stats['出勤天数']).replace(np.inf, 0).round(2) return stats def calculate_attendance_metrics(df, denominator): """计算出勤相关指标""" df = df.copy() df['出勤总时长(h)'] = (df['路途时长(分钟)'] + df['实际工作时长(分钟)']) / 60 df['工作总时长(h)'] = df['实际工作时长(分钟)'] / 60 # 按执行人和出发日期分组,计算每天的数据 daily_stats = df.groupby(['执行人', '出发日期']).agg( station_count=('STATION_NAME', 'nunique'), total_attendance_hours=('出勤总时长(h)', 'sum'), total_work_hours=('工作总时长(h)', 'sum') ).reset_index() # 计算每天是否达标(条件1) daily_stats['达标'] = ( (daily_stats['station_count'] >= 4) & (daily_stats['total_attendance_hours'] >= 7) & (daily_stats['total_work_hours'] >= 2) ) # 按执行人聚合计算总指标 executor_stats = daily_stats.groupby('执行人').agg( meet_days=('达标', 'sum'), total_stations=('station_count', 'sum'), total_attendance_hours=('total_attendance_hours', 'sum'), total_work_hours=('total_work_hours', 'sum') ).reset_index() # 计算衍生指标 executor_stats['平均每天到站址'] = executor_stats['total_stations'] / denominator executor_stats['日均出勤出工时长'] = executor_stats['total_attendance_hours'] / denominator executor_stats['日均有效出勤工时'] = executor_stats['total_work_hours'] / denominator # 添加条件2达标标识 executor_stats['条件2达标'] = ( (executor_stats['平均每天到站址'] >= 4) & (executor_stats['日均出勤出工时长'] >= 7) & (executor_stats['日均有效出勤工时'] >= 2) ) return executor_stats.rename(columns={'meet_days': '出勤达标天数'}) def generate_stats(df, dimension, time_level): """通用统计生成函数""" time_key = { 'daily': '出发日期', 'weekly': '自然周', 'monthly': '自然月' }[time_level] group_keys = dimension['keys'] + [time_key] # 获取维度的主键(用于合并的键名) dimension_key = dimension['keys'][0] # 取维度的第一个键作为合并主键 stats = _calculate_base_stats(df, group_keys) stats['出勤天数'] = df.groupby(group_keys)['出发日期'].nunique().values # 计算月度分母(MIN(TIME_RANGE["end"]的日期部分, 22)) denominator = None if time_level == 'daily': stats['出发日期'] = pd.to_datetime(stats['出发日期']).dt.strftime('%Y-%m-%d') else: stats['出勤天数'] = df.groupby(group_keys)['出发日期'].nunique().values if time_level == 'monthly': # 移除原工作日天数计算,替换为新分母 end_date = datetime.strptime(TIME_RANGE["end"], '%Y-%m-%d %H:%M:%S') end_day = end_date.day denominator = min(end_day, 22) stats = _add_derived_metrics(stats, time_level) # 当维度为执行人且时间粒度为月度时,合并出勤指标(使用正确的键名) if dimension['name'] == '执行人' and time_level == 'monthly' and denominator is not None: attendance_metrics = calculate_attendance_metrics(df, denominator) # 使用维度的实际键名(如'执行人')作为合并键,而非固定的'维度' stats = pd.merge( stats, attendance_metrics[[ # 保留原始'执行人'列作为合并键 '执行人', '出勤达标天数', '平均每天到站址', '日均出勤出工时长', '日均有效出勤工时' ]], on=dimension_key, # 动态使用维度的键名(如'执行人')进行合并 how='left' ) return stats # -------------------------- # 代维统计增强模块 # -------------------------- def enhance_maintainer_stats(filtered_data, member_list_df_filter): time_range_data = filtered_data.copy() # 初始化维度框架 GROUP_KEYS = ['所属组织', '代维简称', '地市', '地市代维'] dim_dfs = [] for key in GROUP_KEYS: df = member_list_df_filter.groupby(key).agg( 应出勤人数=('登陆账号', 'count') ).reset_index() dim_dfs.append(df) if not time_range_data.empty: # ===================== 原有统计逻辑 ===================== # 执行人级别统计 executor_stats = time_range_data.groupby(['执行人', '所属组织', '代维简称', '地市', '地市代维']).agg( 总工单数=('工单编号', 'nunique'), 有效工单数=('工作量评定', lambda s: s.eq('有效').sum()), 总路途时长=('路途时长(分钟)', 'sum'), 总工作时长=('实际工作时长(分钟)', 'sum'), 总出勤天数=('出发日期', 'nunique'), 故障工单数=('工单分类', lambda s: s.eq('故障工单').sum()), 超长退服工单数=('是否超长退服', lambda s: s.eq('是').sum()), 高频故障工单数=('高频故障', lambda s: s.eq('是').sum()), 延期工单数=('是否延期有效', lambda s: s.eq('是').sum()), 故障及时数=('是否及时处理', lambda s: s.eq('是').sum()) ).reset_index() # 计算执行人级别指标 metrics = [ ('平均路途时长(小时)', (executor_stats['总路途时长'] / 60) / executor_stats['总工单数'].replace(0, np.nan)), ('工作时长占比', executor_stats['总工作时长'] / (executor_stats['总路途时长'] + executor_stats['总工作时长']).replace(0, np.nan)), ('人均每日工单数', executor_stats['总工单数'] / executor_stats['总出勤天数'].replace(0, np.nan)), ('人均每日有效工单数', executor_stats['有效工单数'] / executor_stats['总出勤天数'].replace(0, np.nan)), ('人均每日出勤时长(小时)', (executor_stats['总路途时长'] + executor_stats['总工作时长']) / 60 / executor_stats['总出勤天数'].replace( 0, np.nan)), ('人均每日工作时长(小时)', executor_stats['总工作时长'] / 60 / executor_stats['总出勤天数'].replace(0, np.nan)), ('每工单路途时长(小时)', executor_stats['总路途时长'] / 60 / executor_stats['总工单数'].replace(0, np.nan)), ('每工单工作时长(小时)', executor_stats['总工作时长'] / 60 / executor_stats['总工单数'].replace(0, np.nan)), ('超长退服工单占比', executor_stats['超长退服工单数'] / executor_stats['故障工单数'].replace(0, np.nan)), ('高频故障工单占比', executor_stats['高频故障工单数'] / executor_stats['故障工单数'].replace(0, np.nan)), ('延期工单占比', executor_stats['延期工单数'] / executor_stats['故障工单数'].replace(0, np.nan)), ] for col, formula in metrics: executor_stats[col] = formula executor_stats = executor_stats.fillna(0).round(4) # 维度聚合统计 def calculate_dimension_stats(grouped_df, current_key): return grouped_df.groupby(current_key).agg( 总工单数=('总工单数', 'sum'), 有效工单数=('有效工单数', 'sum'), 工单平均路途超2小时人数=('平均路途时长(小时)', lambda x: (x > 2).sum()), 平均工作时长占比低人数=('工作时长占比', lambda x: (x < 0.1).sum()), 人均每日工单数=('人均每日工单数', 'mean'), 人均每日有效工单数=('人均每日有效工单数', 'mean'), 人均每日出勤时长=('人均每日出勤时长(小时)', 'mean'), 人均每日工作时长=('人均每日工作时长(小时)', 'mean'), 每工单路途时长=('每工单路途时长(小时)', 'mean'), 每工单工作时长=('每工单工作时长(小时)', 'mean'), 超长退服工单占比=('超长退服工单占比', 'mean'), 高频故障工单占比=('高频故障工单占比', 'mean'), 延期工单占比=('延期工单占比', 'mean'), ).reset_index().round(4) # 合并维度统计 updated_dims = [] for i, key in enumerate(GROUP_KEYS): dim_stats = calculate_dimension_stats(executor_stats, key) dim_stats['有效工单占比'] = (dim_stats['有效工单数'] / dim_stats['总工单数']).replace(np.inf, 0).fillna( 0).round(4) merged = pd.merge( dim_dfs[i], dim_stats, on=key, how='left' ).fillna(0) updated_dims.append(merged) dim_dfs = updated_dims # ===================== 新增需求处理 ===================== # 提取问题工单 sign_in_issue = time_range_data[time_range_data['原因'] == '签到距离过大'] short_duration_issue = time_range_data[time_range_data['原因'] == '工作时长过短'] top50_travel_orders = time_range_data.nlargest(50, '路途时长(分钟)')['工单编号'].unique() top50_travel_data = time_range_data[time_range_data['工单编号'].isin(top50_travel_orders)] # 处理各维度指标 for i, key in enumerate(GROUP_KEYS): # 合并签到距离过大条目数 sign_in_counts = sign_in_issue.groupby(key)['工单编号'].nunique().reset_index(name='签到距离过大条目数') dim_dfs[i] = pd.merge(dim_dfs[i], sign_in_counts, on=key, how='left') # 合并工作时长过短条目数 short_duration_counts = short_duration_issue.groupby(key)['工单编号'].nunique().reset_index( name='工作时长过短条目数') dim_dfs[i] = pd.merge(dim_dfs[i], short_duration_counts, on=key, how='left') # 合并路途时长TOP50条目数 top50_counts = top50_travel_data.groupby(key)['工单编号'].nunique().reset_index(name='路途时长TOP50条目数') dim_dfs[i] = pd.merge(dim_dfs[i], top50_counts, on=key, how='left') dim_dfs[i] = dim_dfs[i].fillna(0) # ===================== 出勤相关统计 ===================== # 实际出勤人数 def merge_attendance(source_df, target_df, grp_key): att = source_df.groupby(grp_key)['执行人'].nunique().reset_index(name='实际出勤人数') return pd.merge(target_df, att, on=grp_key, how='left').fillna(0) for i, key in enumerate(GROUP_KEYS): dim_dfs[i] = merge_attendance(time_range_data, dim_dfs[i], key) dim_dfs[i]['出勤率'] = (dim_dfs[i]['实际出勤人数'] / dim_dfs[i]['应出勤人数']).replace(np.inf, 0).fillna( 0).round(4) # 出勤天数统计 for i, key in enumerate(GROUP_KEYS): att_cols = ['执行人', key, '出发日期'] att_days = time_range_data[att_cols].drop_duplicates(subset=['执行人', '出发日期']) # 计算每个执行人的出勤天数 att_days_per_executor = att_days.groupby([key, '执行人']).size().reset_index(name='出勤天数') # 总出勤人天(所有人的出勤天数总和) total_attendance_person_days = att_days_per_executor.groupby(key)['出勤天数'].sum().reset_index( name='总出勤人天') dim_dfs[i] = pd.merge(dim_dfs[i], total_attendance_person_days, on=key, how='left').fillna(0) # 出勤天数(不同出发日期的数量) attendance_days = att_days.groupby(key)['出发日期'].nunique().reset_index(name='出勤天数_实际') dim_dfs[i] = pd.merge(dim_dfs[i], attendance_days, on=key, how='left').fillna(0) # 计算人均出勤天数和日均出勤人数 dim_dfs[i]['人均出勤天数'] = (dim_dfs[i]['总出勤人天'] / dim_dfs[i]['实际出勤人数']).replace(np.inf, 0).fillna( 0).round(2) dim_dfs[i]['日均出勤人数'] = (dim_dfs[i]['总出勤人天'] / dim_dfs[i]['出勤天数_实际']).replace(np.inf, 0).fillna( 0).round(2) # 出勤不足3天人数统计 under3 = att_days_per_executor[att_days_per_executor['出勤天数'] < 3].groupby(key)[ '执行人'].count().reset_index(name='出勤不足3天人数') dim_dfs[i] = pd.merge(dim_dfs[i], under3, on=key, how='left').fillna(0) dim_dfs[i]['出勤不足3天人员占比'] = (dim_dfs[i]['出勤不足3天人数'] / dim_dfs[i]['实际出勤人数']).replace( [np.inf, -np.inf], 0).fillna(0).round(4) # ===================== 故障处理及时率计算 ===================== for i, key in enumerate(GROUP_KEYS): fault_data = time_range_data[time_range_data['工单分类'] == '故障工单'] # 初始化要计算的占比列 ratio_columns = ['故障处理及时率', '超长退服工单占比', '高频故障工单占比', '延期工单占比'] if not fault_data.empty: agg_dict = { '是否及时处理': lambda x: x.eq('是').any(), '是否超长退服': lambda x: x.eq('是').any(), '高频故障': lambda x: x.eq('是').any(), '是否延期有效': lambda x: x.eq('是').any(), '所属组织': 'first', '代维简称': 'first', '地市': 'first', '地市代维': 'first' } if key == '地市代维': agg_dict['地市'] = 'first' agg_dict['代维简称'] = 'first' unique_faults = fault_data.groupby('工单编号').agg(agg_dict).reset_index() # 按维度key分组统计 fault_stats_current = unique_faults.groupby(key).agg( 总故障工单数=('工单编号', 'nunique'), 总故障及时数=('是否及时处理', 'sum'), 超长退服工单数=('是否超长退服', 'sum'), 高频故障工单数=('高频故障', 'sum'), 延期工单数=('是否延期有效', 'sum') ).reset_index() # 计算各项占比 fault_stats_current['故障处理及时率'] = ( fault_stats_current['总故障及时数'] / fault_stats_current['总故障工单数'] ).replace([np.inf, -np.inf], 0).fillna(0) fault_stats_current['超长退服工单占比'] = ( fault_stats_current['超长退服工单数'] / fault_stats_current['总故障工单数'] ).replace([np.inf, -np.inf], 0).fillna(0) fault_stats_current['高频故障工单占比'] = ( fault_stats_current['高频故障工单数'] / fault_stats_current['总故障工单数'] ).replace([np.inf, -np.inf], 0).fillna(0) fault_stats_current['延期工单占比'] = ( fault_stats_current['延期工单数'] / fault_stats_current['总故障工单数'] ).replace([np.inf, -np.inf], 0).fillna(0) else: # 创建空数据框并初始化所有占比列为0 fault_stats_current = pd.DataFrame(columns=[key] + ratio_columns) for col in ratio_columns: fault_stats_current[col] = 0.0 # 输出统计结果 logger.info(f"维度[{key}]故障统计结果:") logger.info(f" 总故障工单数: {fault_stats_current.get('总故障工单数', 0).sum()}") logger.info(f" 超长退服工单数: {fault_stats_current.get('超长退服工单数', 0).sum()}") logger.info(f" 高频故障工单数: {fault_stats_current.get('高频故障工单数', 0).sum()}") logger.info(f" 延期工单数: {fault_stats_current.get('延期工单数', 0).sum()}") # 使用map函数手动添加占比数据 for col in ratio_columns: mapping = dict(zip(fault_stats_current[key], fault_stats_current[col])) dim_dfs[i][col] = dim_dfs[i][key].map(mapping).fillna(0) # 输出合并后占比 logger.info(f"维度[{key}]合并后占比:") for col in ratio_columns: mean_value = dim_dfs[i][col].mean() logger.info(f" {col}: {mean_value}") # ===================== 调整字段顺序 ===================== base_columns = [ '应出勤人数', '实际出勤人数', '出勤率', '出勤不足3天人数', '出勤不足3天人员占比', '总工单数', '有效工单数', '有效工单占比', '签到距离过大条目数', '工作时长过短条目数', '路途时长TOP50条目数', '人均出勤天数', '日均出勤人数', '工单平均路途超2小时人数', '平均工作时长占比低人数', '人均每日工单数', '人均每日有效工单数', '人均每日出勤时长', '人均每日工作时长', '每工单路途时长', '每工单工作时长', '超长退服工单占比', '高频故障工单占比', '延期工单占比', '故障处理及时率' ] # 确保所有列都存在 for i, key in enumerate(GROUP_KEYS): # 只保留数据框中实际存在的列 available_columns = [col for col in base_columns if col in dim_dfs[i].columns] ordered_columns = [key] + available_columns dim_dfs[i] = dim_dfs[i][ordered_columns] # ===================== 总计行处理 ===================== for i in range(len(dim_dfs)): df = dim_dfs[i] group_key = GROUP_KEYS[i] # 创建总计行字典 total_row = {group_key: '总计'} # 处理需要sum的字段 sum_cols = [ '应出勤人数', '实际出勤人数', '出勤不足3天人数', '总工单数', '有效工单数', '签到距离过大条目数', '工作时长过短条目数', '路途时长TOP50条目数', '工单平均路途超2小时人数', '平均工作时长占比低人数' ] # 检查并添加出勤相关列 has_total_attendance = '总出勤人天' in df.columns has_attendance_days = '出勤天数_实际' in df.columns if has_total_attendance: sum_cols.append('总出勤人天') if has_attendance_days: sum_cols.append('出勤天数_实际') # 计算总计行的和 for col in sum_cols: if col in df.columns: total_row[col] = df[df[group_key] != '总计'][col].sum() # 计算平均值字段 mean_cols = [ '人均每日工单数', '人均每日有效工单数', '人均每日出勤时长', '人均每日工作时长', '人均出勤天数', '日均出勤人数', '每工单路途时长', '每工单工作时长', '超长退服工单占比', '高频故障工单占比', '延期工单占比', '故障处理及时率' ] for col in mean_cols: if col in df.columns: # 使用加权平均计算总计行的平均值 if '实际出勤人数' in df.columns: total_row[col] = (df[df[group_key] != '总计'][col] * df[df[group_key] != '总计'][ '实际出勤人数']).sum() / df[df[group_key] != '总计']['实际出勤人数'].sum() else: total_row[col] = df[df[group_key] != '总计'][col].mean() # 重新计算总计行中的百分比和比率字段 # 出勤率 total_actual = total_row.get('实际出勤人数', 0) total_expected = total_row.get('应出勤人数', 0) total_row['出勤率'] = total_actual / total_expected if total_expected > 0 else 0 # 出勤不足3天人员占比 total_under3 = total_row.get('出勤不足3天人数', 0) total_row['出勤不足3天人员占比'] = total_under3 / total_actual if total_actual > 0 else 0 # 有效工单占比 total_valid = total_row.get('有效工单数', 0) total_orders = total_row.get('总工单数', 0) total_row['有效工单占比'] = total_valid / total_orders if total_orders > 0 else 0 # 添加总计行到数据框 total_df = pd.DataFrame([total_row]) dim_dfs[i] = pd.concat([df, total_df], ignore_index=True) return tuple(dim_dfs) # -------------------------- # 输出模块 # -------------------------- def save_to_excel(dataframes, path): """增强版Excel输出(带百分比格式)""" with pd.ExcelWriter(path, engine='xlsxwriter') as writer: workbook = writer.book # 定义格式(新增垂直居中设置) header_format = workbook.add_format({ 'bold': True, 'text_wrap': True, # 自动换行 'border': 1, 'bg_color': '#D9EAD3', # 浅绿色背景 'align': 'center', # 水平居中 'valign': 'vcenter' # 垂直居中(新增) }) cell_format = workbook.add_format({ 'border': 1, 'align': 'center', # 内容水平居中 'valign': 'vcenter' # 内容垂直居中(新增) }) percent_format = workbook.add_format({ 'num_format': '0.00%', # 百分比格式 'border': 1, 'align': 'center', # 内容水平居中 'valign': 'vcenter' # 内容垂直居中(新增) }) for name, df in dataframes.items(): # 写入数据时添加标题格式 df.to_excel( writer, sheet_name=name, index=False, header=False, # 禁用自动标题 startrow=1 # 从第二行开始写数据 ) worksheet = writer.sheets[name] # 动态识别百分比列 percent_columns = [ col for col in df.columns if col.endswith(('率', '占比')) ] # 设置列格式(包含标题和数据) for col_num, col_name in enumerate(df.columns): col_width = max(12, len(col_name) * 1.5) # 动态列宽 # 设置标题 worksheet.write(0, col_num, col_name, header_format) # 设置列格式 if col_name in percent_columns: fmt = percent_format else: fmt = cell_format # 应用格式到整列 worksheet.set_column( first_col=col_num, last_col=col_num, width=col_width, cell_format=fmt ) # 冻结首行 worksheet.freeze_panes(1, 0) # 自动筛选(仅对数据量小于10000行的sheet启用) if len(df) < 10000: worksheet.autofilter(0, 0, len(df), len(df.columns) - 1) # -------------------------- # 主流程 # -------------------------- def main(): engine = init_db_engine() try: with time_monitor("数据加载"): site_orders = load_site_orders(engine) order_numbers = site_orders['工单编号'].unique().tolist() order_dfs = batch_load_order_data(engine, order_numbers) with time_monitor("数据合并与处理"): merged_data = merge_order_data(site_orders, order_dfs) if merged_data.empty: logger.warning("无有效数据") return processed_data = ( merged_data .pipe(process_coordinates) .pipe(calculate_distances) .pipe(merge_cran_data, cran_path=FILE_PATHS["cran_info"]) .pipe(generate_time_features) .pipe(calculate_workload) ) output_dfs = {'原始数据': processed_data} with time_monitor("统计计算"): for dim in DIMENSIONS: for time_level in ['daily', 'weekly', 'monthly']: stats_df = generate_stats( processed_data, dimension=dim, time_level=time_level ) level_name = { 'daily': '日统计', 'weekly': '周统计', 'monthly': '月统计' }[time_level] sheet_name = f"{level_name}_{dim['name']}" output_dfs[sheet_name] = stats_df with time_monitor("处理代维组织数据"): member_list_df = pd.read_excel(member_list_path, usecols=member_list_columns, engine='calamine') pattern = r'(?=.*无线2023)(?=.*维护组)(?!.*OLT)' member_list_df = member_list_df[member_list_df['所属组织'].str.contains(pattern, regex=True, na=False)] member_list_df['代维简称'] = member_list_df['所属组织'].str.split("-").str[0] member_list_df['代维简称'] = member_list_df['代维简称'].replace('中移铁通', '铁通') member_list_df = member_list_df.rename(columns={'所属地市': '地市'}) member_list_df['地市代维'] = member_list_df['地市'].str.split("-").str[0] + '-' + member_list_df['代维简称'] member_list_df = member_list_df.drop_duplicates(subset=['登陆账号']) member_list_df['时间戳'] = source_time member_list_df_filter = member_list_df[member_list_df['在职状态'] == '在职'] filtered_data = processed_data[processed_data['所属组织'].isin(member_list_df_filter['所属组织'])] output_dfs['原始数据_副本'] = filtered_data.copy() stats_group, stats_company, stats_city, stats_area = enhance_maintainer_stats( filtered_data, member_list_df_filter ) output_dfs.update({ '代维组维度': stats_group, '代维公司维度': stats_company, '地市维度': stats_city, '地市代维维度': stats_area }) with time_monitor("生成Top50统计"): # 从原始数据_副本中提取Top50 top50_travel = filtered_data.nlargest(50, '路途时长(分钟)') # 选择需要的列 top50_travel = top50_travel[['工单编号', '执行人', '所属组织', '地市', '代维简称', '路途时长(分钟)']] output_dfs['路途时长Top50'] = top50_travel # 在指定的结果表第一列添加时间戳 target_sheets = ['代维组维度', '代维公司维度', '地市维度', '地市代维维度', '路途时长Top50'] for sheet in target_sheets: df = output_dfs[sheet] df.insert(0, '时间戳', source_time) with time_monitor("保存结果"): save_to_excel(output_dfs, FILE_PATHS["output"]) logger.info(f"结果已保存至:{FILE_PATHS['output']}") except Exception as e: logger.error(f"处理失败:{str(e)}", exc_info=True) raise if __name__ == "__main__": main()
07-17
<think>我们正在处理一个复杂的ETL(提取、转换、加载)和数据统计任务。代码的主要流程包括: 1. 配置数据库连接、文件路径、时间范围等。 2. 从数据库加载现场执行工单数据,并进行初步过滤(如排除高铁组织、替换代维简称等)。 3. 根据工单号批量加载其他相关工单数据(如保障、巡检、拆站、验收、发电、故障等工单表)。 4. 合并现场工单和其他工单数据,并进行数据清洗和转换(如计算路途时长和实际工作时长,过滤无效状态等)。 5. 生成时间特征(如自然年、自然月、自然周等)。 6. 处理坐标数据,计算签到距离(现场签到位置与资源位置的距离)和最近CRAN机房的距离。 7. 根据规则评定工作量(有效/无效)。 8. 按不同维度(执行人、所属组织、地市、代维简称、地市代维)和时间粒度(日、周、月)进行统计计算。 9. 加载代维人员信息,并基于此进行代维组织的统计增强(包括出勤率、故障处理及时率等指标)。 10. 生成Top50路途时长的工单。 11. 将所有结果保存到Excel文件中,并对百分比列进行格式化。 由于代码量很大,我们只针对部分关键函数进行解释,并给出代码结构。 注意:由于实际环境限制,我们无法运行依赖数据库和本地文件的代码,因此以下代码主要展示逻辑结构。 我们将重点解释以下部分: 1. 主流程(main函数)的步骤。 2. 数据加载(load_site_orders, batch_load_order_data)。 3. 数据合并与处理(merge_order_data, process_coordinates, calculate_distances, merge_cran_data, generate_time_features, calculate_workload)。 4. 统计计算(generate_stats,以及按维度和时间粒度统计)。 5. 代维统计增强(enhance_maintainer_stats)。 6. 输出(save_to_excel)。 由于代码很长,我们只选择部分关键函数进行展示和解释。 首先,我们看一下主流程:</think>这段代码实现了一个复杂的ETL(提取、转换、加载)和数据分析系统,主要功能是从多个数据源提取工单数据,进行清洗、转换和增强,最后生成多维度的统计报表。以下是关键部分的详细解释: ### 核心功能解析 #### 1. 数据加载模块 ```python def load_site_orders(engine): """加载现场执行工单数据""" sql = text(""" SELECT 地市, 代维简称, 专业, 工单类型, 工单编号, 执行人, 所属组织, 出发时间, 出发经度, 出发纬度, 签到时间, 签到经度, 签到纬度, 完成时间, 完成经度, 完成纬度, 路途时长, 实际工作时长 FROM 现场执行工单 WHERE 出发时间 BETWEEN :start_time AND :end_time """) # ...过滤高铁组织、替换代维名称等处理... ``` #### 2. 工单有效性验证 ```python def calculate_workload(df): """工作量评定""" df['工作量评定'] = '有效' df['原因'] = '' # 无完成时间 no_completion = df['完成时间'].isna() df.loc[no_completion, ['工作量评定', '原因']] = ['无效', '无完成时间'] # 工作时长过短 short_work = df['实际工作时长(分钟)'] < 5 df.loc[short_work, ['工作量评定', '原因']] = ['无效', '工作时长过短'] # 签到距离过大 invalid_dist = ( ~df['维护分类'].isin(['发电', '保障']) & (df['签到距离'] > 300) & (df['签到距离_CRAN'] > 300) ) df.loc[invalid_dist, ['工作量评定', '原因']] = ['无效', '签到距离过大'] return df ``` #### 3. 多维统计计算 ```python def generate_stats(df, dimension, time_level): """通用统计生成函数""" # 确定时间粒度 time_key = {'daily': '出发日期', 'weekly': '自然周', 'monthly': '自然月'}[time_level] group_keys = dimension['keys'] + [time_key] # 基础指标计算 stats = _calculate_base_stats(df, group_keys) # 衍生指标计算 stats = _add_derived_metrics(stats, time_level) # 执行人月度统计特殊处理 if dimension['name'] == '执行人' and time_level == 'monthly': attendance_metrics = calculate_attendance_metrics(df, denominator) stats = pd.merge(stats, attendance_metrics, ...) return stats ``` #### 4. 距离计算工具 ```python def calculate_distance(coord1, coord2): """带校验的距离计算""" try: lat1, lon1 = coord1 lat2, lon2 = coord2 except (TypeError, ValueError): return 99999.0 # 无效坐标处理 # 坐标有效性校验 if not (is_valid_coordinates(lat1, lon1) and is_valid_coordinates(lat2, lon2)): return 99999.0 # 使用geodesic计算实际距离 return geodesic(coord1, coord2).meters ``` #### 5. 增强的Excel输出 ```python def save_to_excel(dataframes, path): """增强版Excel输出(带百分比格式)""" with pd.ExcelWriter(path, engine='xlsxwriter') as writer: # 创建专业格式 header_format = workbook.add_format({'bold': True, 'bg_color': '#D9EAD3'}) percent_format = workbook.add_format({'num_format': '0.00%'}) for name, df in dataframes.items(): # 动态设置列格式 percent_cols = [col for col in df.columns if col.endswith(('率', '占比'))] for col_num, col_name in enumerate(df.columns): if col_name in percent_cols: worksheet.set_column(col_num, col_num, None, percent_format) ``` ### 系统优势 1. **模块化设计**:各功能模块分离,便于维护和扩展 2. **鲁棒性处理**: - 坐标有效性验证 - SQL查询重试机制 - 异常数据处理 3. **性能优化**: - 批量数据加载 - 耗时监控 - 内存高效处理 4. **灵活的统计分析**: - 多维度(执行人/组织/地市等) - 多时间粒度(日/周/月) - 自定义指标计算 ### 使用示例 ```python # 主执行流程 if __name__ == "__main__": engine = init_db_engine() # 初始化数据库连接 site_orders = load_site_orders(engine) # 加载工单数据 merged_data = merge_order_data(site_orders, order_dfs) # 合并数据源 processed_data = process_coordinates(merged_data) # 坐标处理 stats = generate_stats(processed_data, ...) # 生成统计 save_to_excel(stats, "output.xlsx") # 输出结果 ``` ### 优化建议 1. **配置管理**:考虑使用configparser或.env文件管理配置项 2. **并行处理**:对独立任务使用multiprocessing加速 3. **缓存机制**:对不变的基础数据添加缓存 4. **类型提示**:为函数添加类型提示增强可读性 5. **单元测试**:添加pytest测试用例验证核心逻辑
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值