Типове и липсващи стойности
Накратко: Всяка колона в pandas има тип (dtype), който решава какво може с нея, а липсващите стойности (NaN) се намират с isna и се попълват с fillna или махат с dropna.
Проблем
„В кой ден поръчват най-много?“
Управителят иска да знае кога да има повече хора на склада. Във файла year_orders.csv са 11 116 поръчки за една година, всяка с дата и час. Изглежда като една команда: вземи деня от датата и преброй.
Грешка. dtypes казва защо: ordered_at е str, текст. За нас „2025-11-01 07:20“ е дата, но read_csv вижда цифри, тирета и двоеточие и не гадае.
Типът на колоната решава какво можеш да правиш с нея. В този урок ще видиш как се сменя, кои типове пестят памет и дават ред, и какво става, когато стойност липсва.
Идеята
Всяка колона има тип: astype
Всяка колона на DataFrame има един тип (dtype). Видя int64 и float64 за числа и str за текст. Има още bool за True и False, datetime64 за дати и category за малко на брой повтарящи се стойности.
read_csv познава типа от текста във файла. Когато сбърка или когато ти трябва друг, astype прави колона от нов тип. В products категорията е float64, защото подаръчната карта няма категория, а NaN е дробно число. Целите числа с липси пази типът Int64 (с главно I):
Числата отново са цели, а липсата е <NA>: знакът за „няма“ на новите типове в pandas. С astype(int) щеше да получиш грешка IntCastingNaNError, защото обикновеният int няма място за липса.
Виж го
Малко стойности, много редове: category
Колоната status има 11 116 реда, но само пет различни думи. Като текст всяка дума се пази във всеки ред. Типът category пази петте думи веднъж, а за всеки ред само номера на думата:
377 057 байта стават 11 345, около 33 пъти по-малко. В годината на магазина това е дребно, но в таблица с милиони редове е разликата между „влиза в паметта“ и „не влиза“.
Категорията има и втора полза: ред. Статусите не са азбука, а път: нова, платена, изпратена, доставена. CategoricalDtype с ordered=True казва този ред:
value_counts(sort=False) брои в реда на пътя, а не по брой. Сравнението < 'delivered' значи „преди доставката“: 205 поръчки са още на път. Отказаните са сложени накрая, встрани от пътя.
Пробвай
Статусите по пътя
Направи колоната status категория с реда new, paid, shipped, delivered, cancelled и запиши в status_counts колко поръчки има на всяка стъпка, в този ред.
Решение
import pandas as pd
orders = pd.read_csv('year_orders.csv')
steps = pd.CategoricalDtype(['new', 'paid', 'shipped', 'delivered', 'cancelled'], ordered=True)
orders['status'] = orders['status'].astype(steps)
status_counts = orders['status'].value_counts(sort=False)
status_countspd.CategoricalDtype(['new', 'paid', 'shipped', 'delivered', 'cancelled'], ordered=True) задава реда, astype(steps) го слага на колоната, а value_counts(sort=False) брои по него: 90 нови, 63 платени, 52 изпратени, 10 254 доставени и 657 отказани. Само с value_counts() редът е по брой, с доставените отгоре. С astype('category') без списък редът е азбучен: cancelled, delivered, new, paid, shipped.
Идеята
Дати: pd.to_datetime и .dt
pd.to_datetime прави от текста дати. Колоната става от тип datetime64 и вече знае календара. Частите на датата дава .dt: .dt.year, .dt.month, .dt.day_name() и още много:
Годината е от 1 ноември 2025, 7:20, до 31 октомври 2026, 23:01. Първата поръчка е в събота. .dt работи само върху дати: върху текста от началото даде грешката.
day_name() дава имената на английски, както ще ги видиш и в документацията. Повече за датите (по седмици, по месеци, плъзгащо средно) има в урока за времето.
Идеята
Текст: .str
Каквото .dt е за дати, .str е за текст: прилага метод на низа към всеки ред наведнъж. str.contains вече видя във филтрите. Има още str.lower() и str.upper(), str.strip() (маха интервалите в краищата), str.len(), str.split() и изрязване като при низ: str[:14].
Етикетите на рафта побират 14 знака. Кои имена не се побират и как ще изглеждат отрязани?
7 имена са по-дълги от 14 знака. str[:14] ги реже, а str.upper() ги прави с главни букви за етикета. Резултатът е пак колона, затова assign я слага до името.
Идеята
Липсващи стойности: isna и notna
В почти всяка истинска таблица нещо липсва: клиент не е казал града си, касата не е пратила отчет. pandas пише липсата като NaN (при дати NaT, при новите типове <NA>). isna() дава маска True там, където липсва, а .sum() върху нея брои липсите във всяка колона:
Един клиент няма град. notna() е обратната маска: True там, където има стойност. С двете маски филтрираш, както с всяко условие.
Виж го
Защо 0 не е „липсва“
Баницата и краставиците са с наличност 0. Липса ли е това?
isna() не намира нищо в stock: нулите са истински стойности. 0 значи „знаем, че няма нито една“, а NaN значи „не знаем“.
Сега оборотът по дни от урока за обобщенията, но като Series в pandas. Една важна разлика: NumPy даваше nan за средната, а pandas прескача липсите:
count() брои 6 известни дни от 7. Средната на тях е 494,70 €, както с np.nanmean. Сумата 2 968,20 € обаче тихо е без сряда: pandas не предупреждава. Със fillna(0) средната пада на 424,03 €, защото нулата казва „в сряда не продадохме нищо“, а това не е вярно.
По-точно, за любопитните
Прескачането се управлява с параметъра skipna: revenue.mean(skipna=False) дава nan, като NumPy. Полезно е, когато предпочиташ сметката да „светне“ при липса, вместо тихо да даде по-малко число.
Идеята
fillna, dropna или нищо
Има три пътя с липсата, и изборът зависи от въпроса, а не от кода:
fillna(стойност), когато знаеш какво значи липсата. В отчет „клиенти по град“ липсващият град е „неизвестен“: той е отговор, не измислица.dropna(), когато редът не може да отговори на въпроса. В отчет по категории подаръчната карта няма къде да отиде.subset=[...]гледа само дадените колони.- Нищо, когато не знаеш. Оборотът в сряда не е 0, а средната на известните дни е по-честна.
dropna махна подаръчната карта: от 25 продукта остават 24, колкото са и notna() в категорията. В отчет „колко продукта продаваме“ това би била грешка. fillna('неизвестен') прави липсващия град отделен ред в value_counts.
Пробвай
Клиентите по град
Маркетингът иска да знае откъде са клиентите. Запиши в missing_city колко клиенти нямат град, а в by_city колко клиенти има във всеки град, като липсващият град се брои като „неизвестен“.
Решение
import pandas as pd
customers = pd.read_csv('customers.csv')
missing_city = customers['city'].isna().sum()
by_city = customers['city'].fillna('неизвестен').value_counts()
print(missing_city)
by_citycustomers['city'].isna().sum() дава 1. customers['city'].fillna('неизвестен').value_counts() брои всичките 20 клиенти: 6 в София, 4 в Пловдив и така нататък, и 1 „неизвестен“. Без fillna value_counts прескача NaN и брои 19, а count() дава 19, броя на клиентите с град, не без.
Упражнение
Денят с най-много поръчки
Отговори на управителя. Направи ordered_at дата, сложи в нова колона day името на деня от седмицата, запиши в by_day колко поръчки има във всеки ден и в best_day деня с най-много поръчки.
Решение
import pandas as pd
orders = pd.read_csv('year_orders.csv')
orders['ordered_at'] = pd.to_datetime(orders['ordered_at'])
orders['day'] = orders['ordered_at'].dt.day_name()
by_day = orders['day'].value_counts()
best_day = by_day.index[0]
print(best_day)
by_daypd.to_datetime прави датите, а .dt.day_name() дава деня. value_counts() подрежда от най-много към най-малко, затова by_day.index[0] е Friday: 1 890 поръчки. Най-слаба е неделята с 1 280. Броени са всички поръчки, и отказаните: въпросът е кога идват, не колко струват. С .dt.day щеше да получиш 31 реда, по един за всеки ден от месеца.
Тест
Провери се
Свързани уроци
Раздел „Свързани уроци“- Нужно отпреди: Series и DataFrame · Избиране: [], loc и iloc · Филтри и подреждане · Нови колони
- Продължава в: Групи: groupby · Съединяване: merge · Широко и дълго: pivot_table и melt · Дати и времеви редове
- Задачи по темата: Таблици с pandas