1. 项目概述:为什么Pandas里“空”比你想象的更复杂
“数据分析-Pandas的类型数据的缺失处理”——这标题看着平平无奇,但我在带某高校数据科学实训课时发现,超过七成的学员卡在同一个地方:不是不会写df.dropna(),而是根本没意识到,同一个“空”,在不同数据类型下,Pandas的底层处理逻辑、内存表现、计算行为甚至报错方式,全都不一样。有人把字符串列里的''当成缺失值去填,结果fillna('未知')后,''还在;有人对含pd.NaT的时间列做groupby().mean(),程序直接报TypeError: mean is not implemented for this dtype;还有人用isnull()检查一个含None的object列,结果漏掉了np.nan——因为None和np.nan在object类型里是两种独立存在的“空”。这不是操作不熟,是根本没建立起类型敏感的缺失值认知框架。这个项目要解决的,就是把Pandas中所有类型(numeric、string、datetime、category、boolean、object)下的缺失值表现、检测逻辑、填充策略、聚合影响、内存开销全部掰开揉碎讲透。它适合三类人:刚学完read_csv就急着跑模型的新手,总被SettingWithCopyWarning折磨的中级使用者,以及需要部署稳定数据清洗Pipeline的工程师。核心不是教命令,而是建立一套“看到数据类型,立刻能预判缺失行为”的直觉。
2. 核心设计思路:类型驱动的缺失处理范式
2.1 为什么不能统一用np.nan?——类型系统的硬约束
很多人初学时有个朴素想法:“既然np.nan是通用缺失标记,那我把所有列都转成float,不就统一了?”实测下来,这在工程上是灾难性的。我曾接手一个某实验室的气象数据清洗脚本,原始数据里有整数温度(如25)、浮点气压(如1013.25)、字符串站点名(如"Beijing_A")和时间戳(如2023-01-01 08:00:00)。原作者为图省事,把所有列强制astype(float),结果站点名全变成nan,时间戳变成1.82e9这种毫无业务意义的数字,后续所有地理标签和时间切片全崩。根本原因在于:Pandas的类型系统(dtype)不是装饰,而是内存布局和运算规则的契约。int64类型在内存中每个值占8字节,且不允许存储np.nan——因为np.nan本质是一个特殊的浮点数位模式(IEEE 754标准中的quiet NaN),而整数类型没有定义NaN位。所以当你对一个纯整数列执行df['col'].iloc[0] = np.nan时,Pandas会自动将该列dtype升级为float64,并把原整数25变成25.0。这个隐式转换看似无害,但代价巨大:内存占用翻倍(int64→float64),且丢失了整数语义(比如你无法再用df['col'].dtypes == 'int64'来快速筛选计数类字段)。因此,我们的设计起点必须是:尊重原始dtype,为每种类型定制缺失处理方案,而不是强行归一化。
2.2 四层缺失值识别体系:从物理存储到逻辑语义
Pandas的缺失值检测不是简单的“等于np.nan”,而是一套分层机制。我把它拆解为四层,每一层对应不同的技术实现和业务含义:
物理层(Physical Layer):这是最底层的内存表示。
np.nan(float系列)、pd.NaT(datetime/timedelta系列)、pd.NA(pandas 1.0+引入的通用缺失值,支持string/boolean/category)各自有独立的二进制编码。None在object列中是Python对象指针为空,与np.nan的位模式完全不同。这一层决定了df.values数组里实际存的是什么。API层(API Layer):Pandas暴露给用户的检测函数。
pd.isna()是唯一推荐的通用检测器,它内部会根据dtype调用不同分支:对float列用np.isnan(),对datetime列用pd.isnat(),对string列(pandas 1.0+)用pd.isna()专用逻辑。而pd.isnull()是pd.isna()的别名,但df['col'].isnull()在旧版本中可能对某些扩展类型失效,所以统一用pd.isna()。语义层(Semantic Layer):这是业务层面的理解。比如一个订单状态列,
''(空字符串)和'cancelled'都是有效值,但None才代表“状态未录入”。此时pd.isna()能正确捕获None,但df['status'] == ''才是检测业务意义上的“缺失”。这一层必须由分析师结合业务文档定义,无法靠代码自动推断。聚合层(Aggregation Layer):缺失值如何影响计算结果。
df['num'].sum()默认跳过np.nan,但df['cat'].mode()遇到pd.NA会返回空Series,df['bool'].all()遇到pd.NA会返回pd.NA而非True/False。这一层决定了你的统计口径是否可靠。
这套分层体系意味着:没有“一刀切”的缺失处理方案,只有“按类型选工具,按业务定语义,按聚合看影响”的三维决策矩阵。后面所有实操,都基于这个框架展开。
2.3 方案选型逻辑:pd.NAvsnp.nanvsNone的实战取舍
Pandas 1.0引入pd.NA作为新的缺失值标准,但很多教程还在教np.nan。到底该用哪个?我的经验是:新项目一律用pd.NA,老项目迁移需评估成本。理由很实在:
pd.NA是真正的三值逻辑(True/False/NA),df['col'] > 5遇到pd.NA返回pd.NA,而np.nan > 5返回False(这是IEEE 754的坑,np.nan == np.nan为False,但np.nan > 5也False,导致逻辑混乱)。pd.NA支持更多类型:string、boolean、category列原生支持pd.NA,无需转object。而np.nan只能塞进float或object列,强转string列会变'nan'字符串。但迁移有成本:
pd.NA在pandas < 1.0的环境会报错;某些旧库(如xlwings)读写Excel时可能把pd.NA转成#N/A错误值;pd.NA参与算术运算会返回pd.NA(如pd.NA + 5),而np.nan + 5返回np.nan,前者更符合逻辑,但需要代码适配。
所以我的实操建议是:新建DataFrame时,显式指定dtype="string"、dtype="boolean"等,并用pd.NA填充;对已存在的np.nan数据,用df = df.convert_dtypes()一键升级(该方法会将float列的np.nan转为pd.NA,同时将int列的np.nan转为Int64可空整数类型)。这比手动替换安全得多。
3. 核心细节解析:六种数据类型的缺失行为全解
3.1 数值型(numeric):int64、float64、Int64的生死线
数值型是缺失处理的“主战场”,但三种子类型差异极大:
float64:天然支持np.nan,pd.isna()返回True,df['col'].sum()自动忽略。但问题在于:np.nan是浮点数,df['col'].dtype永远是float64,无法表达“本应是整数但缺失”的语义。比如用户年龄列,25.0和np.nan混在一起,后续做df['age'].astype(int)会报错。int64:绝对不允许np.nan。一旦插入np.nan,Pandas强制升为float64。这是新手最常见的内存暴增原因。解决方案是使用pandas的可空整数类型Int64(注意首字母大写)。Int64底层用pd.NA表示缺失,df['age'] = pd.array([25, 30, pd.NA], dtype="Int64"),df['age'].dtype显示Int64,内存占用与int64接近,且df['age'].sum()返回55(跳过pd.NA)。Int64(可空整数):这是现代Pandas处理整数缺失的黄金标准。但要注意:Int64是extension dtype,df['col'].values返回的是IntegerArray对象,不是numpy数组。如果下游代码依赖np.ndarray,需用df['col'].to_numpy(dtype=float, na_value=np.nan)转换。
实操中,我处理数值列的标准化流程是:
- 读取时用
pd.read_csv(..., dtype={'age': 'Int64', 'score': 'float64'})显式声明; - 检测缺失用
pd.isna(df['col']); - 填充用
df['col'].fillna(0)(Int64列填0保持类型,填np.nan会转回float64); - 聚合前确认:
df['col'].sum(skipna=True)是默认行为,无需额外参数。
提示:
Int64列的fillna()只能填标量(如0、-1)或同类型Series,不能填np.nan。若误填df['col'].fillna(np.nan),Pandas会静默转为float64,且不报错——这是个隐蔽的陷阱,务必用df['col'].dtype二次确认。
3.2 字符串型(string):告别object,拥抱stringdtype
传统做法把字符串列设为objectdtype,但这带来三大问题:一是pd.isna()能检测None和np.nan,但漏掉''(空字符串);二是str方法(如.str.upper())遇到None会报AttributeError;三是内存效率低,每个字符串都是独立Python对象。
pandas 1.0+的stringdtype是革命性改进。它原生支持pd.NA,且str方法对pd.NA返回pd.NA,不报错。创建方式很简单:df['name'] = pd.array(['Alice', 'Bob', pd.NA], dtype="string")。此时:
pd.isna(df['name'])返回[False, False, True];df['name'].str.len()返回[5, 3, <NA>];df['name'].fillna('Unknown')返回['Alice', 'Bob', 'Unknown'],dtype保持string。
但要注意兼容性:stringdtype在pandas < 1.0不可用;读取CSV时,pd.read_csv(..., dtype={'name': 'string'})要求pandas ≥ 1.0;若数据中有bytes类型,stringdtype会报错,需先df['col'] = df['col'].astype(str)清洗。
我处理字符串列的黄金步骤:
- 强制转换:
df['col'] = df['col'].astype("string")(自动将None/np.nan转为pd.NA); - 业务缺失清洗:
df['col'] = df['col'].replace('', pd.NA)(把空字符串也纳入缺失); - 统一填充:
df['col'] = df['col'].fillna('N/A'); - 验证:
df['col'].isna().sum()应为0,df['col'].dtype应为string。
注意:
astype("string")会将None转为pd.NA,但np.nan在object列中会被转为字符串'nan'!所以转换前,务必先用df['col'] = df['col'].replace({np.nan: pd.NA})清理np.nan。
3.3 时间型(datetime):NaT的精确打击与陷阱
时间列的缺失值是pd.NaT(Not a Time),它是datetime64的专属NaN。关键特性:
pd.isna(pd.NaT)返回True,但pd.NaT == pd.NaT返回False(同np.nan);pd.NaT参与算术运算会返回pd.NaT,如pd.NaT + pd.Timedelta('1D');- 最致命的是:
pd.NaT在groupby().agg()中会导致整个组聚合失败,报TypeError。
实操中,我见过最典型的坑是计算用户平均在线时长。原始数据有login_time和logout_time两列,类型为datetime64[ns]。直接df['duration'] = df['logout_time'] - df['login_time'],遇到任一列为NaT,结果就是NaT。但后续df.groupby('user_id')['duration'].mean()会报错,因为mean()不支持timedelta64与NaT混合。
解决方案分三步:
- 安全减法:用
pd.to_timedelta()包装,df['duration'] = (df['logout_time'] - df['login_time']).where(df['logout_time'].notna() & df['login_time'].notna(), pd.NaT); - 安全聚合:
df.groupby('user_id')['duration'].apply(lambda x: x.dropna().mean()),显式dropna(); - 类型强化:
df['duration'] = df['duration'].astype("timedelta64[ns]"),确保dtype正确。
提示:
pd.to_datetime()读取含空值的字符串时间列时,errors='coerce'参数会将无法解析的值(如''、'???')转为NaT,这是最安全的解析方式,比errors='ignore'(保留原字符串)靠谱得多。
3.4 分类型(category):pd.NA的语义保真
category类型用于存储有限、重复的字符串(如['red', 'blue', 'green']),优势是内存小、排序快。但传统category不支持缺失值——pd.Categorical(['a', 'b', np.nan])会把np.nan当作一个类别值,categories里出现nan,这完全违背缺失语义。
pandas 1.0+的categorydtype原生支持pd.NA。创建方式:cat = pd.Categorical(['a', 'b', pd.NA], categories=['a', 'b'])。此时:
cat.isna()返回[False, False, True];cat.remove_unused_categories()不会删除pd.NA(它不是类别);pd.get_dummies(cat)会生成a、b两列,pd.NA行全为0。
实操要点:
- 读取时指定
dtype={'color': 'category'},然后df['color'] = df['color'].astype("category"),再df['color'] = df['color'].cat.add_categories(pd.NA)(不推荐,add_categories不接受pd.NA); - 正确做法:先用
pd.array(..., dtype="string")创建字符串列,再df['col'] = df['col'].astype("category"),pd.NA会自动保留; - 填充时,
df['col'].fillna('Other')会将pd.NA转为字符串'Other',若'Other'不在原categories中,Pandas会自动添加,df['col'].cat.categories会多出'Other'。
注意:
category列的fillna()会改变categories,若需保持categories不变,应用df['col'] = df['col'].cat.add_categories('Other').fillna('Other'),先扩容再填充。
3.5 布尔型(boolean):三值逻辑的落地
布尔列传统用object或booldtype,但bool不支持缺失(np.nan会强制转float64),object则失去向量化运算优势。pandas 1.0+的booleandtype是救星,它支持True/False/pd.NA。
创建:df['flag'] = pd.array([True, False, pd.NA], dtype="boolean")。此时:
df['flag'].sum()返回1(pd.NA被忽略);df['flag'].all()返回pd.NA(因为True & False & NA逻辑不确定);~df['flag'](取反)对pd.NA返回pd.NA,而非False。
实操中,布尔列缺失常源于条件判断:df['is_adult'] = df['age'] >= 18,但age列有pd.NA,结果is_adult列全是pd.NA。正确写法是:df['is_adult'] = df['age'].ge(18, fill_value=False),fill_value=False表示当age为pd.NA时,比较结果为False。
提示:
booleandtype的fillna()只能填True、False或pd.NA,填1会报错。df['flag'].fillna(True)是安全的,df['flag'].fillna(1)会抛TypeError。
3.6 对象型(object):最后的堡垒与清理战场
objectdtype是万能容器,能装任何Python对象,但也是缺失处理的“灰色地带”。它的缺失值可以是None、np.nan、pd.NA、甚至自定义对象。pd.isna()能检测None和np.nan,但对pd.NA在旧版本可能失效。
处理object列的核心原则:尽一切可能降级为专用dtype。流程如下:
- 检查内容:
df['col'].apply(type).value_counts(),看是否混杂多种类型(如str、int、None); - 若纯字符串:
df['col'] = df['col'].astype("string"); - 若纯数字:
df['col'] = pd.to_numeric(df['col'], errors='coerce')(errors='coerce'将无法转的值变np.nan); - 若纯时间:
df['col'] = pd.to_datetime(df['col'], errors='coerce'); - 若确实无法归类(如嵌套字典),保留
object,但用df['col'] = df['col'].where(df['col'].apply(lambda x: x is not None and not (isinstance(x, float) and np.isnan(x))))显式清理None和np.nan。
注意:
object列的fillna()填'N/A'后,df['col'].dtype仍是object,但'N/A'是字符串,None是NoneType,类型不一致。最好统一转为stringdtype再填充。
4. 实操过程:从原始数据到生产就绪的完整流水线
4.1 数据加载阶段:源头控制缺失形态
很多缺失问题其实在read_csv()时就埋下了雷。我坚持的加载规范是:
# 推荐:显式声明dtype,启用高效解析 df = pd.read_csv( "data.csv", dtype={ "user_id": "Int64", # 可空整数 "name": "string", # 字符串 "score": "float64", # 浮点数 "status": "category", # 分类 "is_active": "boolean", # 布尔 "created_at": "datetime64[ns]" # 时间 }, parse_dates=["created_at"], # 时间列单独解析 date_parser=lambda x: pd.to_datetime(x, errors='coerce'), # 容错解析 keep_default_na=False, # 关键!禁用默认na_values na_values=["", "NULL", "null", "N/A", "n/a"] # 显式定义哪些字符串算缺失 )keep_default_na=False是关键开关。默认情况下,pandas会把['', '#N/A', '#N/A N/A', '#NA', '-1.#IND', '-1.#QNAN', '-NaN', '-nan', '1.#IND', '1.#QNAN', 'N/A', 'NA', 'NULL', 'NaN', 'nan', 'null']都视为缺失,这在真实数据中极易误伤(比如用户昵称叫'NULL')。显式定义na_values,只把业务确认的缺失标记纳入。
加载后立即验证:
# 检查每列缺失率 missing_report = pd.DataFrame({ 'dtype': df.dtypes, 'total': len(df), 'missing_count': df.isna().sum(), 'missing_pct': (df.isna().sum() / len(df) * 100).round(2) }) print(missing_report.sort_values('missing_pct', ascending=False))4.2 缺失诊断阶段:定位“真缺失”与“假缺失”
df.isna().sum()只告诉你数量,不告诉你原因。我必做的三步诊断:
分布分析:
df['col'].value_counts(dropna=False),看pd.NA/None/np.nan是否与其他值共存。例如,status列有['active', 'inactive', <NA>, 'active'],说明缺失是随机的;若只有<NA>,可能是整列未采集。关联分析:
df[df['col'].isna()][['col1', 'col2']],看缺失是否集中在某些组合。比如payment_method缺失时,order_amount总是0,说明是免费订单,缺失是合理的,应填'free'而非插补。模式分析:对时间列,用
df.set_index('date_col')['col'].plot(),看缺失是否呈周期性(如每周日缺失),这暗示ETL流程缺陷,需修复上游。
实操心得:我见过一个电商数据集,
product_category列缺失率30%,但df[df['product_category'].isna()]['product_name'].str.contains('gift').sum()高达95%。结论:缺失是因为礼品卡类商品无分类,业务上应填'gift_card',而非用众数填充。
4.3 缺失填充阶段:策略选择与参数精调
填充不是“填上就行”,而是“填得合理”。我按缺失率和业务逻辑分三级:
低缺失率(<5%):直接删除或简单填充。
- 删除:
df = df.dropna(subset=['col'], how='any'),但需记录删除行数,print(f"Dropped {len(original_df)-len(df)} rows"); - 填充:数值列用
df['col'].fillna(df['col'].median())(抗异常值),字符串列用df['col'].fillna(df['col'].mode()[0])(众数)。
- 删除:
中缺失率(5%-30%):按分组填充,保留业务结构。
# 例:按地区填充平均房价 df['price'] = df.groupby('region')['price'].transform( lambda x: x.fillna(x.median()) )高缺失率(>30%):必须分析原因,通常需业务介入。技术上可用模型插补,但仅限于数值列:
from sklearn.experimental import enable_iterative_imputer from sklearn.impute import IterativeImputer imputer = IterativeImputer(max_iter=10, random_state=0) # 仅对数值列插补 numeric_cols = df.select_dtypes(include=[np.number]).columns df[numeric_cols] = imputer.fit_transform(df[numeric_cols])
关键参数:max_iter控制迭代次数,sample_posterior=True启用贝叶斯采样(更鲁棒),但计算慢。实测max_iter=5对大多数数据足够。
4.4 类型强化阶段:convert_dtypes()的深度应用
df.convert_dtypes()是pandas的“类型净化器”,但它有隐藏参数。默认调用:
df = df.convert_dtypes( infer_objects=True, # 尝试将object列转为string/boolean等 convert_string=True, # 启用string dtype convert_integer=True, # 启用Int64等可空整数 convert_boolean=True, # 启用boolean dtype convert_floating=True # 启用floating dtype(实验性) )但要注意:convert_floating=True会将float64转为Float64(可空浮点),这在某些计算中可能不兼容。我的实践是关闭它,只用前四项。
调用后,必须验证:
# 检查是否所有列都升级了 upgraded = df.dtypes != original_df.dtypes print("Upgraded dtypes:") print(df.dtypes[upgraded]) # 检查缺失值是否统一为pd.NA print("All missing values are pd.NA:", df.isna().sum().sum() == df.isna().sum().sum()) # 看似冗余,实为确认4.5 生产就绪检查:缺失处理的最终防线
上线前,我运行一个“缺失健康检查”函数:
def check_missing_health(df): issues = [] # 1. 检查是否有object列残留 object_cols = df.select_dtypes(include=['object']).columns.tolist() if object_cols: issues.append(f"Warning: object columns remain: {object_cols}") # 2. 检查数值列是否意外含pd.NA(应为Int64/Float64) num_cols = df.select_dtypes(include=[np.number]).columns for col in num_cols: if df[col].isna().any() and not str(df[col].dtype).startswith('Int') and not str(df[col].dtype).startswith('Float'): issues.append(f"Error: Numeric column '{col}' has np.nan but dtype is {df[col].dtype}") # 3. 检查字符串列是否含空字符串(业务缺失) string_cols = df.select_dtypes(include=['string']).columns for col in string_cols: if (df[col] == '').sum() > 0: issues.append(f"Warning: String column '{col}' contains empty strings") # 4. 检查时间列是否含NaT但未解析 datetime_cols = df.select_dtypes(include=['datetime64']).columns for col in datetime_cols: if df[col].isna().sum() > 0 and df[col].dt.tz is None: issues.append(f"Warning: Datetime column '{col}' has NaT but no timezone") return issues # 运行检查 health_issues = check_missing_health(df) if health_issues: print("Missing Health Check FAILED:") for issue in health_issues: print(f" - {issue}") else: print("✅ Missing Health Check PASSED")这个检查覆盖了90%的线上缺失事故。有一次,它提前发现了user_id列虽为Int64,但df['user_id'].isna().sum()为0,而业务方反馈有10%用户ID未采集——原来上游ETL把缺失ID写成了0,而非pd.NA。这属于业务逻辑缺失,需修正上游,而非数据清洗。
5. 常见问题与排查技巧实录
5.1 典型问题速查表
| 问题现象 | 根本原因 | 解决方案 | 我的实操记录 |
|---|---|---|---|
df['col'].fillna(0)后,df['col'].dtype变成float64 | col原为int64,fillna(0)触发隐式转换 | 改用df['col'] = df['col'].astype("Int64").fillna(0) | 某金融项目,用户ID列被转float,导致下游API校验失败,耗时2小时定位 |
df.groupby('cat')['num'].mean()报TypeError: mean is not implemented | num列为timedelta64或含NaT | 改用df.groupby('cat')['num'].apply(lambda x: x.dropna().mean()) | 某物流系统,配送时长列含NaT,mean()直接崩溃 |
df['str_col'].str.upper()报AttributeError: 'NoneType' object has no attribute 'upper' | str_col为objectdtype,含None | 先df['str_col'] = df['str_col'].astype("string"),再.str.upper() | 某CRM系统,客户姓名列含None,字符串操作全挂 |
pd.isna(df['col'])返回False,但df['col'].iloc[0]显示nan | col为objectdtype,nan是字符串'nan',非np.nan | 用df['col'] = df['col'].replace('nan', pd.NA)清洗 | 某爬虫数据,'nan'字符串被误认为缺失,实际是有效值 |
df['bool_col'].all()返回pd.NA,预期True/False | bool_col为booleandtype,含pd.NA,三值逻辑下all()不确定 | 改用df['bool_col'].fillna(True).all(),明确pd.NA视为True | 某风控系统,用户协议列含pd.NA,all()返回pd.NA导致审批流中断 |
5.2 独家避坑技巧
技巧1:用
df.info(memory_usage='deep')监控内存
在convert_dtypes()前后各运行一次,对比内存变化。Int64列内存应接近原int64,若翻倍,说明有列被错误转为float64。我曾用此法发现一个score列因含'N/A'字符串,被astype("Int64")失败后静默转为object,内存暴增5倍。技巧2:
pd.array()构造测试数据,隔离问题
遇到诡异缺失行为,不要在全量数据上调试。用arr = pd.array([1, 2, pd.NA], dtype="Int64")构造最小复现案例,快速验证fillna()、sum()等行为。这比df.head()高效十倍。技巧3:
df._mgr.blocks窥探底层存储
这是pandas内部属性,可查看数据实际存储块。df._mgr.blocks[0].values显示第一块的numpy数组。若看到[1, 2, nan],说明是float64;若看到[1, 2, <NA>],说明是Int64。这是终极诊断手段,适用于debug底层dtype问题。技巧4:
warnings.filterwarnings("error")捕获静默转换
在关键清洗步骤前加import warnings; warnings.filterwarnings("error"),这样df['col'].fillna(0)触发int64→float64转换时,会抛FutureWarning并中断,强迫你处理。上线前移除即可。
5.3 性能优化实录:百万行数据的缺失处理
处理100万行数据时,df.isna().sum()要3秒,df.fillna()要5秒,太慢。我的优化方案:
向量化替代循环:避免
for idx in df.index: if pd.isna(df.loc[idx, 'col']): ...,改用mask = df['col'].isna(); df.loc[mask, 'col'] = value。分块处理:对超大文件,用
pd.read_csv(..., chunksize=50000)分块读取,每块独立清洗,再pd.concat()。query()加速过滤:df.query('col != col')比df[df['col'].isna()]快20%,因为query()编译为numexpr表达式。categorical加速分组:对高基数字符串列,先df['col'] = df['col'].astype("category"),groupby()速度提升3倍。
实测:某电商日志表(80万行,20列),原始清洗耗时42秒;应用上述优化后,降至9秒。关键提速点是query()和category转换。
6. 工程化延伸:缺失处理的自动化与监控
6.1 构建缺失处理配置文件
手工写fillna()不可维护。我用YAML定义缺失策略:
# missing_config.yaml columns: user_id: dtype: "Int64" fill_strategy: "constant" fill_value: -1 name: dtype: "string" fill_strategy: