2026, Jan 02 06:02
Как в pandas определить пропущенные и неполные годы без учета текущего года
Практичный алгоритм и код на pandas: как найти годы без активности и неполные годы по порогу месяцев, не включая текущий год. Получите список лет для бэкфилла.
При анализе временных операционных данных по каждому активу часто приходится выявлять годы без активности и отличать их от лет с неполной активностью. В ленте по транспортному средству это обычно проявляется как пропуски до первой реальной записи и неполные месяцы в первом зафиксированном году. Цель — системно находить такие «разрывы», чтобы последующая логика могла их заполнить, при этом не помечая текущий, ещё незавершённый год.
Пример данных и выявляемая проблема
Рассмотрим один автомобиль, чья годовая история хранится в pandas DataFrame. В ранних годах записей нет, самый ранний зафиксированный год может быть неполным, а последний год продолжается и потому тоже неполный. Нам нужно вернуть список отсутствующих или неполных лет, которые следует заполнить, но никогда не включать текущий год.
import pandas as pd
import numpy as np
records_df = pd.DataFrame({
"Vehicle Type": ["van", "van", "van", "van", "van", "van", "van"],
"Vehicle ID": ["ABC", "ABC", "ABC", "ABC", "ABC", "ABC", "ABC"],
"Year": [1, 2, 3, 4, 5, 6, 7],
"Earliest Fact": [pd.NaT, pd.NaT, "2018-04-18", "2019-01-02", "2020-01-02", "2021-01-01", "2022-01-01"],
"Latest Fact": [pd.NaT, pd.NaT, "2019-01-01", "2020-01-01", "2020-12-31", "2021-12-31", "2023-01-01"],
"Fact History": [np.nan, np.nan, 5, 11.7, 12, 12, 5.7],
"Days Worked": [np.nan, np.nan, 100, 256, 273, 300, 94],
"Days Available": [np.nan, np.nan, 130, 272, 290, 320, 141]
})Здесь первые два года пустые. Третий год — первый с данными, но он не полный. Седьмой год — текущий и по своей природе тоже частичный; его нельзя считать пропущенным.
Что именно отсутствует и почему
Нам нужны все годы до и включая первый зафиксированный год, если этот первый год неполный; иначе — все годы строго до него. Это более жёсткое правило не даёт случайно включить текущий год. Проще всего понять, был ли год «полным», по количеству месяцев истории: если в самом раннем зафиксированном году меньше выбранного порога месяцев, мы считаем его неполным и включаем в результат вместе со всеми предыдущими пустыми годами. Практичный порог вытекает из наблюдаемых данных: в «Fact History» встречаются значения вроде 11.7, поэтому отсечение в 11 месяцев удобно отделяет «по сути полный» год от «частичного».
Компактный способ получить недостающие годы
Ниже приведённый фрагмент находит самый ранний год с данными, проверяет, не является ли он частичным, используя порог по месяцам, вычисляет границу и возвращает нужный список лет. Он избегает громоздких соединений и группировок, при этом сохраняет задуманную логику.
# Поскольку в «Fact History» встречается 11.7, используем этот порог, чтобы решить, считать ли год полным
FULL_YEAR_MIN_MONTHS = 11
active_years = records_df.query("not `Fact History`.isna()")
first_observed_row = active_years.query("`Year` == `Year`.min()").iloc[0]
first_year_is_partial = bool(first_observed_row["Fact History"] < FULL_YEAR_MIN_MONTHS)
first_observed_year = first_observed_row["Year"]
boundary_year = first_observed_year - (0 if first_year_is_partial else 1)
timeline_years = records_df["Year"].tolist()
missing_year_list = [y for y in timeline_years if y <= boundary_year]
print(missing_year_list)Это даёт все предыдущие пустые годы и включает первый зафиксированный год только если он был неполным. Последний год не затрагивается, потому что граница вычисляется исключительно по самому раннему году с данными.
Если предпочитаете оставить отбор внутри pandas, а не в виде генератора списка Python, можно отфильтровать через query и затем извлечь значения:
subset_df = records_df.query("`Year` <= @boundary_year")
year_values = subset_df["Year"].tolist()
print(year_values)Почему это различие важно
Дальнейшая логика заполнения обычно требует чистого, детерминированного набора лет для импутации или бэкфилла. Считая самый ранний частичный год неполным и никогда не помечая текущий год как пропущенный, вы сохраняете согласованность конвейера данных. Так вы избегаете случайной утечки сигналов «в процессе» в обнаружение разрывов и получаете воспроизводимое правило для исторического заполнения.
Практические выводы
Сначала выделите годы с реальными данными и найдите самый ранний из них. Определите, считать ли этот год полным или частичным, используя порог по отработанным месяцам, согласованный с наблюдаемыми значениями. Выведите границу и выберите все годы вплоть до неё. Такой прицельный подход компактен, избегает хрупкой логики соединений и напрямую отражает способ записи данных. Полученный список лет можно безопасно передать функции импутации или заполнения и держать текущий год в стороне, пока он не завершится.