Чистене на данни
Накратко: Чистенето на данни маха дубликатите, привежда цени, дати и имена в един формат и проверява с код, че след почистването данните са верни.
Проблем
Новата ценова листа
Нов доставчик прати ценовата си листа като CSV. Управителят пита две неща: колко средно ще плащаме за продукт и колко ни струва кафето. Файлът supplier.csv е до кода. Ето го:
Всичките четири колони са str. Цените са и „0,95 €“, и „1.45“, и празно, и „по запитване“. Датите са в два формата. Сиренето е два пъти. Имената са с главни, малки и интервали. Кафето струва 1190.
prices['price'].mean() дори не тръгва: цените са текст. Такива данни се казват мръсни и са нормалното, а не изключението. Програмата на доставчика пише както пише, а хората попълват на ръка.
Идеята
Почистването е код, не ръчна работа
Изкушението е да отвориш файла и да поправиш клетките на ръка. Но след месец доставчикът ще прати нова листа със същите грешки. Затова почистването е код: върви наново върху всеки нов файл.
Пътят е един и същ:
- Виж какво не е наред:
dtypes,describe,value_counts, подреждане. - Поправи с една стъпка за всеки вид мръсотия: дубликати, текст, числа, дати, изключения.
- Провери с код, че е станало: броят редове преди и след, NaN само там, където трябва.
Всяка стъпка от този урок е за един от видовете мръсотия в листата.
Идеята
Дубликати: duplicated и drop_duplicates
duplicated() дава маска: True за всеки ред, който повтаря някой ред над себе си. .sum() ги брои. С subset= сравнява само дадените колони, а keep=False маркира всички копия, не само второто:
Съвсем еднакъв ред има един: сиренето, P07. По код обаче повторенията са две. Прясното мляко, P05, е два пъти, веднъж като „ПРЯСНО МЛЯКО“ и веднъж като „ Прясно мляко“, с интервал отпред. За pandas това са различни текстове, затова duplicated() не го хваща.
drop_duplicates() маха повторенията и оставя първия ред. Но ако го пуснеш сега, млякото ще остане два пъти.
Идеята
Имена: str.strip и str.lower
Текстът се чисти с .str от урока за типовете. str.strip() маха интервалите в краищата, а str.lower() прави всички букви малки. Така „ПРЯСНО МЛЯКО“ и „ Прясно мляко“ стават един и същ текст:
От 16 реда остават 14: сиренето и млякото са махнати. Редът на стъпките е важен: първо се чисти текстът, после се търсят дубликати.
По-точно, за любопитните
str.lower() е за сравняване. За етикет или отчет str.capitalize() прави първата буква главна: „прясно мляко“ става „Прясно мляко“. Ако имената се различават и в средата (два интервала между думите), str.replace(' ', ' ') или str.split().str.join(' ') ги изравнява.
Пробвай
Без повторения
Изчисти имената в листата (без интервали в краищата, с малки букви), махни повторените редове и запиши в rows колко реда остават.
Решение
import pandas as pd
prices = pd.read_csv('supplier.csv')
prices['name'] = prices['name'].str.strip().str.lower()
prices = prices.drop_duplicates()
rows = len(prices)
print(rows)prices['name'].str.strip().str.lower() изравнява имената, а drop_duplicates() после маха двата повторени реда: от 16 остават rows = 14. Ако първо махнеш дубликатите, остават 15: тогава двата реда на млякото още са различни и drop_duplicates маха само сиренето.
Идеята
Цени от текст: str.replace и pd.to_numeric
„0,95 €“ е текст по три причини: знакът €, запетаята вместо точка и интервалът. str.replace маха всяка от тях. После pd.to_numeric прави текста на число. С errors='coerce' всичко, което пак не е число, става NaN, вместо да спре кода:
Цената е float64, а NaN са две: ябълките (празно поле) и минералната вода („по запитване“). Това е вярно: доставчикът не е дал цена за тях. Без errors='coerce' to_numeric спира с ValueError: Unable to parse string "по запитване".
errors='coerce' е удобен и опасен: не казва какво е изхвърлил. Затова броиш NaN преди и след. Ако след превръщането са повече от празните полета, нещо е станало NaN по грешка.
Пробвай
Цените като числа
Направи колоната price число. NaN трябва да остане само там, където доставчикът не е дал цена.
Решение
import pandas as pd
prices = pd.read_csv('supplier.csv')
text = prices['price'].str.replace('€', '').str.replace(',', '.').str.strip()
prices['price'] = pd.to_numeric(text, errors='coerce')
print(prices['price'].isna().sum())str.replace('€', ''), после str.replace(',', '.') и str.strip() правят „0,95 €“ на „0.95“, а pd.to_numeric(text, errors='coerce') го прави на 0.95. NaN остават 2: ябълките и водата. Само с to_numeric(..., errors='coerce') NaN стават 11 от 16, защото всяка цена със запетая тихо изчезва, и кодът не дава никаква грешка.
Виж го
Дати в два формата
Половината дати са „01.11.2026“ (ден, месец, година), а другата половина „2026-11-01“. Ако оставиш pandas да гадае:
pandas гадае формата от първата дата и я чете като %m.%d.%Y: месец отпред, по американски. На ISO датата спира с грешка. Ако всички дати бяха с точки, нямаше да има грешка: 01.11 щеше тихо да стане 11 януари.
Затова казваш формата сам, с format=, по веднъж за всеки формат. С errors='coerce' датите в другия формат стават NaT, а fillna слива двата прочита:
13 реда важат от 1 ноември и 2 от 15 ноември. Една дата не се чете с нито един формат: 31.11.2026 при бананите. Такъв ден няма, ноември има 30 дни.
Виж го
Изключения: describe и подреждане
Цените вече са числа. describe() от първия поглед казва дали изглеждат правдоподобно, а подреждането показва кои са най-големите:
Средната е 102,70 €, а медианата 1,62 €. Такава разлика значи, че една стойност е далеч от останалите. Това се казва изключение (outlier). Подреждането го намира веднага: кафето на зърна за 1190 €. Ние продаваме кафето за 14,90 €, значи доставната цена е 11,90 €: програмата на доставчика е изпуснала десетичната точка.
Поправката е правило за всички цени над 100 €. Пишеш го в кода, а не в една клетка, за да хване същата грешка и в следващата листа:
Кафето струва 11,90 €, средната падна на 4,53 €, а медианата остана 1,62 €. Медианата не усеща едно огромно число, а средната го усеща цялото.
По-точно, за любопитните
Правилото „над 100 € значи изпусната точка“ е вярно за тази листа, защото знаем продуктите: нищо в нея не струва над 100 €. В друга листа (телевизори, мебели) същото правило би развалило верни цени. Когато не знаеш защо стойността е странна, питай откъдето е дошла, вместо да я поправяш.
Идеята
Проверките като код: assert
Почистването е готово, когато можеш да го докажеш. assert условие, 'съобщение' не прави нищо, ако условието е вярно, и спира кода с AssertionError и съобщението, ако не е. Ето цялото почистване с проверките в края:
От 16 реда остават 14. Проверките казват на глас какво очакваш:
- всеки код е веднъж;
- цена липсва само на двата продукта без цена;
- всяка цена е между 0,10 € и 100 €;
- неразчетената дата е само една.
Когато следващата листа дойде с нова грешка, проверката ще спре кода с ясно съобщение, вместо отчетът тихо да излезе грешен.
Упражнение
Средната доставна цена
Отговори на управителя. Изчисти цялата листа в prices: имената и дубликатите, цените като числа, датите в двата формата и кафето без десетичната точка. Продуктите без цена остават, с NaN. Запиши в avg_price средната цена, закръглена до 2 знака.
Решение
import pandas as pd
prices = pd.read_csv('supplier.csv')
prices['name'] = prices['name'].str.strip().str.lower()
prices = prices.drop_duplicates()
text = prices['price'].str.replace('€', '').str.replace(',', '.').str.strip()
prices['price'] = pd.to_numeric(text, errors='coerce')
dots = pd.to_datetime(prices['valid_from'], format='%d.%m.%Y', errors='coerce')
iso = pd.to_datetime(prices['valid_from'], format='%Y-%m-%d', errors='coerce')
prices['valid_from'] = dots.fillna(iso)
cents = prices['price'] > 100
prices.loc[cents, 'price'] = prices.loc[cents, 'price'] / 100
assert prices['code'].is_unique
assert prices['price'].isna().sum() == 2
avg_price = round(prices['price'].mean(), 2)
print(avg_price)Четирите стъпки в този ред дават 14 реда, 2 цени NaN и 1 дата NaT. round(prices['price'].mean(), 2) е 4,53 €, а кафето струва 11,90 €. Без поправката на кафето средната е 102,70 €. С fillna(0) водата и ябълките „струват“ 0 € и дърпат средната надолу, а с dropna() губиш и бананите, които имат вярна цена.
Тест
Провери се
Свързани уроци
Раздел „Свързани уроци“- Нужно отпреди: Избиране: [], loc и iloc · Филтри и подреждане · Нови колони · Типове и липсващи стойности
- Продължава в: Изтичане на данни (data leakage)
- Задачи по темата: Подреждане на данни