3. Pandas处理航行表格数据入门

船舶航行数据通常以表格形式记录:每一行对应一个时刻的航行状态,每一列对应一个字段,例如时间戳、经纬度、对地航速、对地航向、船首向等。Pandas 的 DataFrame 正是为这类“带列名的二维表”设计的结构。本节围绕一个典型的航行 CSV 文件,完成从加载、索引、选列切片到类型修正的完整链路,并输出一份清洗后的数据摘要。

一、航行表格的典型形态与加载

假设船上导出的航行记录文件 voyage_log.csv 包含以下列:timestamp(记录时间,形如 2024-05-12 08:00:00)、lat(纬度,北纬为正)、lon(经度,东经为正)、sog(对地航速,节)、cog(对地航向,度,正北为 0°、顺时针增大)、heading(船首向,度)。需要先明确坐标与角度约定:纬度北正南负、经度东正西负;航向与船首向均以正北为 0°、顺时针 0°~360°。全文与图示保持一致。

import pandas as pd

df = pd.read_csv("voyage_log.csv")
print(df.shape)      # 例如 (1440, 6)
print(df.dtypes)
print(df.head(3))

刚读入时,Pandas 会做类型推断:数值列一般被识别为 int64 或 float64,但时间列通常只是 object(字符串),而航速、航向列如果原始文件里混有单位后缀、空值或异常符号(如 "12.4kn"、""、"--"),也会被推断成 object。因此“读进来”不等于“可用”,类型检查是第一步。

二、索引:让每一行有明确身份

DataFrame 默认使用 0 起始的整数索引(RangeIndex)。对航行数据而言,更自然的行标识是时间戳。可以在读取时直接指定,也可以在解析时间后设置:

df["timestamp"] = pd.to_datetime(df["timestamp"], errors="coerce")
df = df.set_index("timestamp").sort_index()

设置时间索引后,df.loc["2024-05-12 08:00":"2024-05-12 09:00"] 就能按时间区间取行,这是航行数据分析中最常用的切片方式。注意 .loc 的区间切片是闭区间,两端都包含;而位置切片 df.iloc[0:10] 遵循 Python 惯例,不含右端。二者不要混用。另外,set_index 后原 timestamp 列变为索引,若仍需保留为列,可加 drop=False。

三、列选择与行列切片

选列有三种常见写法,语义不同:df["sog"] 返回 Series;df[["sog", "cog"]] 返回只含这两列的 DataFrame(注意双层方括号);df.loc[:, "sog":"heading"] 按列名区间取连续列。行与列同时筛选时,推荐统一用 .loc(按标签)和 .iloc(按位置),避免链式赋值带来的 SettingWithCopyWarning。

# 取 08:00 到 09:00 之间的航速与航向
seg = df.loc["2024-05-12 08:00":"2024-05-12 09:00", ["sog", "cog"]]

# 取前 100 行的经纬度(按位置)
pos = df.iloc[:100, [1, 2]]

条件筛选同样重要,例如只保留航行状态(航速大于 0.5 节)的记录:df[df["sog"] > 0.5]。若条件涉及多列,用 &、| 连接,每个条件加括号。

四、数据类型转换:把“看起来是数”变成“真的是数”

类型修正的目标是:时间列为 datetime64[ns],航速与航向为 float64,并保证取值范围合理。对混有单位或异常符号的列,先用 pd.to_numeric(..., errors="coerce") 强制转换,无法解析的值变为 NaN,再决定填补或剔除。

# 去掉单位后缀再转数值
df["sog"] = pd.to_numeric(
    df["sog"].astype(str).str.replace("kn", "", regex=False),
    errors="coerce"
)
df["cog"] = pd.to_numeric(df["cog"], errors="coerce")

# 航向规范化到 [0, 360)
df["cog"] = df["cog"] % 360

# 统计转换后产生的缺失
print(df[["sog", "cog", "heading"]].isna().sum())

转换后必须做合理性检查:航速不应为负,通常也不应超过船速上限(如 30 节);航向应在 0°~360°;纬度在 −90°~90°,经度在 −180°~180°。可用 df.describe() 快速看分布,再用布尔索引定位越界行。对缺失值,航行数据中常见做法是按时间线性插值(df["sog"].interpolate())或直接剔除,选择取决于后续分析目的,需在摘要中说明处理策略。

航行表格数据清洗链路:从 CSV 到可用 DataFrame voyage_log.csv timestamp object lat/lon float64 sog object cog object heading float64 read_csv 加载 + 设索引 to_datetime set_index sort_index 索引 = 时间戳 选列/切片 类型修正 to_numeric errors="coerce" cog % 360 异常值 → NaN 校验 数据摘要 shape dtypes describe isna().sum 关键列的类型与取值变化(示例) 列名 转换前 转换后 校验要点 timestamp object datetime64[ns] 单调递增、无重复 sog object(含 "kn") float64 0 ≤ sog ≤ 30 cog object(含 "--") float64 0 ≤ cog < 360

五、输出清洗后的数据摘要

清洗完成后,应输出一份可复核的摘要,至少包含:行列数、各列类型、关键列的统计量、缺失值数量、时间范围。下面是一段可直接运行的摘要代码:

summary = {
    "行数": len(df),
    "列数": df.shape[1],
    "时间范围": (df.index.min(), df.index.max()),
    "缺失值": df.isna().sum().to_dict(),
    "航速统计": df["sog"].describe().round(2).to_dict(),
    "航向范围": (df["cog"].min(), df["cog"].max()),
}
for k, v in summary.items():
    print(f"{k}: {v}")

示例输出(基于假设数据,非实测):行数 1440、列数 6,时间范围 2024-05-12 00:00 至 2024-05-12 23:59,sog 缺失 3 条、均值约 11.8 节、最大 18.6 节,cog 范围 0.4°~359.7°。若发现 sog 出现负值或超过 30 节,说明单位或符号解析有误,应回到转换步骤排查,而不是直接进入分析。

索引与切片的三条实用规则:按时间取行用 .loc(闭区间);按位置取行用 .iloc(左闭右开);选多列务必用双层方括号 df[["a","b"]],单层方括号得到的是 Series。
不要对切片结果直接赋值修改原表。若 seg = df.loc[...] 后再写 seg["sog"] = ...,可能触发 SettingWithCopyWarning 且修改不生效。需要修改时用 df.loc[行条件, "列名"] = 值 一步到位,或先 .copy()。

把上述步骤串起来,就形成了航行数据进入分析前的标准动作:读入并确认列类型 → 时间解析并设为索引 → 按需选列与切片 → 数值列强制转换与范围校验 → 输出摘要留档。后续的航迹绘制、航速分布统计、转向事件识别,都建立在这份“类型正确、索引清晰、异常可查”的 DataFrame 之上。