Cleaning data, oftewel het opschonen van data, is een belangrijke bezigheid voor data scientists. Op deze pagina gaan we in op de stappen die je kunt doorlopen in Python wanneer je een dataset op wilt schonen. We maken veel gebruik van het package Pandas.
Data cleaning kunnen we definiëren als het proces om missende of onjuiste data te identificeren en mogelijk aan te passen of te verwijderen.
Binnen veel data science vraagstukken is het opschonen van data hetgeen waar het meeste tijd in gaat zitten. Immers, machine learning modellen geven alleen waardevolle bevindingen wanneer de input voor het model kwalitatief is.
In dit blog gaan we in op vier stappen die je kunt doorlopen:
- Ga op zoek naar missende data
- Bekijk of er outliers zijn
- Vraag je af welke data onnodig zijn
- Zitten er inconsistenties in de dataset?
We zullen in deze tutorial gebruik maken van deze dataset met boekings-informatie van verschillende hotels. De dataset bevat informatie over bijvoorbeeld wanneer een boeking werd gemaakt, de duur van het verblijf, het aantal gasten, etc.
Om een snelle indruk te krijgen van de dataset bekijken we de omvang van de dataset en de verschillende data types per kolom.
import pandas as pd
import numpy as np
import seaborn as sns
import matplotlib.pyplot as plt
df = pd.read_csv('hotel_bookings.csv')
df.head()
hotel | is_canceled | lead_time | arrival_date_year | arrival_date_month | arrival_date_week_number | arrival_date_day_of_month | stays_in_weekend_nights | stays_in_week_nights | adults | ... | deposit_type | agent | company | days_in_waiting_list | customer_type | adr | required_car_parking_spaces | total_of_special_requests | reservation_status | reservation_status_date | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | Resort Hotel | 0 | 342 | 2015 | July | 27 | 1 | 0 | 0 | 2 | ... | No Deposit | NaN | NaN | 0 | Transient | 0.0 | 0 | 0 | Check-Out | 2015-07-01 |
1 | Resort Hotel | 0 | 737 | 2015 | July | 27 | 1 | 0 | 0 | 2 | ... | No Deposit | NaN | NaN | 0 | Transient | 0.0 | 0 | 0 | Check-Out | 2015-07-01 |
2 | Resort Hotel | 0 | 7 | 2015 | July | 27 | 1 | 0 | 1 | 1 | ... | No Deposit | NaN | NaN | 0 | Transient | 75.0 | 0 | 0 | Check-Out | 2015-07-02 |
3 | Resort Hotel | 0 | 13 | 2015 | July | 27 | 1 | 0 | 1 | 1 | ... | No Deposit | 304.0 | NaN | 0 | Transient | 75.0 | 0 | 0 | Check-Out | 2015-07-02 |
4 | Resort Hotel | 0 | 14 | 2015 | July | 27 | 1 | 0 | 2 | 2 | ... | No Deposit | 240.0 | NaN | 0 | Transient | 98.0 | 0 | 1 | Check-Out | 2015-07-03 |
5 rows × 32 columns
# bekijk de omvang van de dataset en de verschillende data types
print(df.shape)
print(df.dtypes)
(119390, 32)
hotel object
is_canceled int64
lead_time int64
arrival_date_year int64
arrival_date_month object
arrival_date_week_number int64
arrival_date_day_of_month int64
stays_in_weekend_nights int64
stays_in_week_nights int64
adults int64
children float64
babies int64
meal object
country object
market_segment object
distribution_channel object
is_repeated_guest int64
previous_cancellations int64
previous_bookings_not_canceled int64
reserved_room_type object
assigned_room_type object
booking_changes int64
deposit_type object
agent float64
company float64
days_in_waiting_list int64
customer_type object
adr float64
required_car_parking_spaces int64
total_of_special_requests int64
reservation_status object
reservation_status_date object
dtype: object
We zien dat er meer dan 100.000 rijen in de dataset zitten en 32 kolommen. Daarnaast krijgen we een goede indruk van de beschikbare informatie in de dataset.
Data cleaning stap 1: zoek de missende data
Data cleaning in Python start vaak met het identificeren van missende data. Deze stap wordt bijna nooit overgeslagen, simpelweg omdat de meeste machine learning modellen niet werken op het moment dat er missende data als input wordt gegeven. Je kunt het identificeren van missende data op verschillende manieren aanpakken.
# missing values visualiseren met een heatmap
plt.figure(figsize=(15,6))
cols = df.columns
sns.heatmap(df[cols].isnull(), cmap="YlGnBu",
cbar_kws={'label': 'Missende data'})
<AxesSubplot:>
We leren uit bovenstaande heatmap dat er relatief weinig missing values in deze dataset zitten. We missen enkele waarden van de 'country' feature en we missen vooral veel onder 'agent' en 'company'.
We kunnen het aantal missende waarden ook overzichtelijk onder elkaar zetten op de volgende manier.
# missing values overzicht per feature
for column in df.columns:
aantal_missing = np.sum(df[column].isnull())
print('{} - {}'.format(column, aantal_missing))
hotel - 0
is_canceled - 0
lead_time - 0
arrival_date_year - 0
arrival_date_month - 0
arrival_date_week_number - 0
arrival_date_day_of_month - 0
stays_in_weekend_nights - 0
stays_in_week_nights - 0
adults - 0
children - 4
babies - 0
meal - 0
country - 488
market_segment - 0
distribution_channel - 0
is_repeated_guest - 0
previous_cancellations - 0
previous_bookings_not_canceled - 0
reserved_room_type - 0
assigned_room_type - 0
booking_changes - 0
deposit_type - 0
agent - 16340
company - 112593
days_in_waiting_list - 0
customer_type - 0
adr - 0
required_car_parking_spaces - 0
total_of_special_requests - 0
reservation_status - 0
reservation_status_date - 0
We leren hieruit dat voor de feature 'children' ook enkele waarden missen. Dat zagen we eerder in de heatmap niet omdat het zo weinig waarden betreft.
Wat doe je met missende waarden?
Per situatie kan verschillen wat het juiste is om te doen. Soms zul je de hele feature niet nodig hebben, soms kun je enkele rijen verwijderen om het probleem op te lossen, en soms kun je de missende waarden vrij nauwkeurig inschatten. We zullen verschillende technieken toepassen op onze dataset.
Enkele rijen verwijderen
We hebben slechts enkele rijen waarbij we data missen over kinderen. Omdat het slechts enkele rijen betreft identificeren we welke rijen dit zijn en verwijderen we de observaties uit de dataset. We zien dat dit leidt tot slechter vier observaties minder.
# identificeer de rij-nummers van observaties waarbij een waarde voor kinderen ontbreekt
print(df.shape)
missende_kinderen = df[df['children'].isnull()].index
df_zonder_missing_children = df.drop(missende_kinderen, axis=0)
print(df_zonder_missing_children.shape)
(119390, 32)
(119386, 32)
Hele feature verwijderen
De feature 'company' mist in 94% van de observaties. We vinden dit te veel binnen onze analyse en zullen daarom de hele feature droppen.
# groot deel van column 'company' ontbreekt. Daarom droppen we de hele feature
percentage_company_missend = np.mean(df_zonder_missing_children['company'].isnull())
print(percentage_company_missend)
df_minder_missing = df_zonder_missing_children.drop(['company'], axis=1)
print(df_minder_missing.shape)
0.943067026284489
(119386, 31)
We zien dat we na deze handeling nog 31 features over hebben.
Missende waarden vervangen
In veel gevallen kun je missende waarden vervangen met een bepaalde waarde. Zo zouden we voor onze feature 'agent' kunnen weten dat alle missende waarden uit deze column te maken hebben met het feit dat de boeking niet door een agent is gedaan maar door een particulier. We kunnen deze particuliere boekingen bijvoorbeeld het nummer 9999 meegeven.
# verschillende agents hebben een uniek nummer
print(df_minder_missing['agent'].unique())
# we vullen missende waarden op met 9999 voor particuliere boekingen
df_minder_missing['agent'] = df_minder_missing['agent'].fillna(9999)
print(df_minder_missing.isnull().sum())
[ nan 304. 240. 303. 15. 241. 8. 250. 115. 5. 175. 134. 156. 243.
242. 3. 105. 40. 147. 306. 184. 96. 2. 127. 95. 146. 9. 177.
6. 143. 244. 149. 167. 300. 171. 305. 67. 196. 152. 142. 261. 104.
36. 26. 29. 258. 110. 71. 181. 88. 251. 275. 69. 248. 208. 256.
314. 126. 281. 273. 253. 185. 330. 334. 328. 326. 321. 324. 313. 38.
155. 68. 335. 308. 332. 94. 348. 310. 339. 375. 66. 327. 387. 298.
91. 245. 385. 257. 393. 168. 405. 249. 315. 75. 128. 307. 11. 436.
1. 201. 183. 223. 368. 336. 291. 464. 411. 481. 10. 154. 468. 410.
390. 440. 495. 492. 493. 434. 57. 531. 420. 483. 526. 472. 429. 16.
446. 34. 78. 139. 252. 270. 47. 114. 301. 193. 182. 135. 350. 195.
352. 355. 159. 363. 384. 360. 331. 367. 64. 406. 163. 414. 333. 427.
431. 430. 426. 438. 433. 418. 441. 282. 432. 72. 450. 180. 454. 455.
59. 451. 254. 358. 469. 165. 467. 510. 337. 476. 502. 527. 479. 508.
535. 302. 497. 187. 13. 7. 27. 14. 22. 17. 28. 42. 20. 19.
45. 37. 61. 39. 21. 24. 41. 50. 30. 54. 52. 12. 44. 31.
83. 32. 63. 60. 55. 56. 89. 87. 118. 86. 85. 210. 214. 129.
179. 138. 174. 170. 153. 93. 151. 119. 35. 173. 58. 53. 133. 79.
235. 192. 191. 236. 162. 215. 157. 287. 132. 234. 98. 77. 103. 107.
262. 220. 121. 205. 378. 23. 296. 290. 229. 33. 286. 276. 425. 484.
323. 403. 219. 394. 509. 111. 423. 4. 70. 82. 81. 74. 92. 99.
90. 112. 117. 106. 148. 158. 144. 211. 213. 216. 232. 150. 267. 227.
247. 278. 280. 285. 289. 269. 295. 265. 288. 122. 294. 325. 341. 344.
346. 359. 283. 364. 370. 371. 25. 141. 391. 397. 416. 404. 299. 197.
73. 354. 444. 408. 461. 388. 453. 459. 474. 475. 480. 449.]
hotel 0
is_canceled 0
lead_time 0
arrival_date_year 0
arrival_date_month 0
arrival_date_week_number 0
arrival_date_day_of_month 0
stays_in_weekend_nights 0
stays_in_week_nights 0
adults 0
children 0
babies 0
meal 0
country 488
market_segment 0
distribution_channel 0
is_repeated_guest 0
previous_cancellations 0
previous_bookings_not_canceled 0
reserved_room_type 0
assigned_room_type 0
booking_changes 0
deposit_type 0
agent 0
days_in_waiting_list 0
customer_type 0
adr 0
required_car_parking_spaces 0
total_of_special_requests 0
reservation_status 0
reservation_status_date 0
dtype: int64
Missende waarden schatten
Voor veel features kun je missende waarden schatten. Zo kun je ervoor kiezen om bijvoorbeeld het gemiddelde, de modus, of de mediaan te gebruiken voor missende waarden van een feature. Verder is het altijd aan te raden of je op een of andere manier de missende waarde logisch kunt achterhalen.
In onze dataset missen we alleen nog 488 waarden voor 'country'. We weten dus niet van alle boekingen uit welk land deze afkomstig is. Wellicht kun je dit makkelijk achterhalen door te kijken naar door welke 'agent' deze boekingen gedaan zijn. Of wellicht komen alle boekingen vanuit dezelfde 'company'. Zo kun je wellicht het land van herkomst achterhalen.
Omdat bijna de helft van de boekingen uit onze dataset komen uit Portugal kiezen we er in dit geval voor de missende 'country' waarden te vullen met de meestvoorkomende 'country', namelijk 'PRT'.
# meestvoorkomende country identificeren en gebruiken voor missende waarden
print(df_minder_missing['country'].value_counts(normalize=True))
df_minder_missing['country'] = df_minder_missing['country'].fillna('PRT')
print(df_minder_missing['country'].value_counts(normalize=True))
PRT 0.408636
GBR 0.102012
FRA 0.087596
ESP 0.072062
DEU 0.061288
...
BWA 0.000008
LCA 0.000008
PYF 0.000008
ASM 0.000008
ATF 0.000008
Name: country, Length: 177, dtype: float64
PRT 0.411053
GBR 0.101595
FRA 0.087238
ESP 0.071767
DEU 0.061037
...
BWA 0.000008
LCA 0.000008
PYF 0.000008
ASM 0.000008
ATF 0.000008
Name: country, Length: 177, dtype: float64
We zien dat de impact van deze handeling op de dataset meevalt.
Data cleaning stap 2: bekijk of er outliers zijn
Data cleaning in Python gaat gepaard met het zoeken naar outliers. Outliers zijn observaties die significant anders zijn dan andere observaties. Een outliers kan op een fout in de dataset wijzen, maar het kan ook een echte waarde zijn die afwijkt. Dan is het aan de data scientist om te bepalen of de waarde in de dataset opgenomen blijft.
Er zijn verschillende manieren om outliers te ontdekken als je data opschoont. De meest geschikte manier verschilt voor numerieke en categorische features. We behandelen diverse methoden.
Descriptive statistics
Descriptive statistics zijn een goede manier om voor numerieke features outliers te identificeren. Met .describe() verkrijg je per feature verschillende beschrijvende informatie m.b.t. de data.
df_minder_missing.describe()
is_canceled | lead_time | arrival_date_year | arrival_date_week_number | arrival_date_day_of_month | stays_in_weekend_nights | stays_in_week_nights | adults | children | babies | is_repeated_guest | previous_cancellations | previous_bookings_not_canceled | booking_changes | agent | days_in_waiting_list | adr | required_car_parking_spaces | total_of_special_requests | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
count | 119386.000000 | 119386.000000 | 119386.000000 | 119386.000000 | 119386.000000 | 119386.000000 | 119386.000000 | 119386.000000 | 119386.000000 | 119386.000000 | 119386.000000 | 119386.000000 | 119386.000000 | 119386.000000 | 119386.000000 | 119386.000000 | 119386.000000 | 119386.000000 | 119386.000000 |
mean | 0.370395 | 104.014801 | 2016.156593 | 27.165003 | 15.798553 | 0.927605 | 2.500310 | 1.856390 | 0.103890 | 0.007949 | 0.031913 | 0.087121 | 0.137102 | 0.221131 | 1443.195953 | 2.321227 | 101.833541 | 0.062520 | 0.571340 |
std | 0.482913 | 106.863286 | 0.707456 | 13.605334 | 8.780783 | 0.998618 | 1.908289 | 0.579261 | 0.398561 | 0.097438 | 0.175770 | 0.844350 | 1.497462 | 0.652315 | 3408.320220 | 17.595011 | 50.534664 | 0.245295 | 0.792798 |
min | 0.000000 | 0.000000 | 2015.000000 | 1.000000 | 1.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 1.000000 | 0.000000 | -6.380000 | 0.000000 | 0.000000 |
25% | 0.000000 | 18.000000 | 2016.000000 | 16.000000 | 8.000000 | 0.000000 | 1.000000 | 2.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 9.000000 | 0.000000 | 69.290000 | 0.000000 | 0.000000 |
50% | 0.000000 | 69.000000 | 2016.000000 | 28.000000 | 16.000000 | 1.000000 | 2.000000 | 2.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 28.000000 | 0.000000 | 94.590000 | 0.000000 | 0.000000 |
75% | 1.000000 | 160.000000 | 2017.000000 | 38.000000 | 23.000000 | 2.000000 | 3.000000 | 2.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 240.000000 | 0.000000 | 126.000000 | 0.000000 | 1.000000 |
max | 1.000000 | 737.000000 | 2017.000000 | 53.000000 | 31.000000 | 19.000000 | 50.000000 | 55.000000 | 10.000000 | 10.000000 | 1.000000 | 26.000000 | 72.000000 | 21.000000 | 9999.000000 | 391.000000 | 5400.000000 | 8.000000 | 5.000000 |
Hier kunnen we direct de volgende observaties doen:
- stays_in_weekend_nights heeft een max van 19. Het is de vraag of je 19 nachten in een weekend kunt blijven. Wellicht is iemand veel weken gebleven, maar waarschijnlijk is dit een foutje.
- Er is een boeking gemaakt met 55 volwassenen. Moest dit niet 5 zijn?
- Er is iemand met 10 babies gekomen, klopt dat?
Boxplots voor identificeren outliers
Met boxplots kun je missende waarden mooi visualiseren. Zo kunnen we de features die ons opvielen visualiseren.
df_minder_missing[['stays_in_weekend_nights','adults','babies']].plot(kind='box')
<AxesSubplot:>
Histogram voor detectie outliers
Ook een histogram kun je goed gebruiken voor het identificeren van outliers. In een histogram kun je vooral goed zien welke verdeling de data volgt en of daar mogelijkerwijs oneffenheden inzitten.
df_minder_missing['lead_time'].plot(kind='hist')
<AxesSubplot:ylabel='Frequency'>
Voor categorische variabele kun je het best een bar chart gebruiken voor visualisatie.
df_minder_missing['hotel'].value_counts().plot(kind='bar')
<AxesSubplot:>
Qua data cleaning kun je ook voor outliers verschillende werkwijzen hanteren. Je kunt outliers in de dataset laten zitten, je kunt ze vervangen door passender schattingen, of je kunt ze verwijderen. Wat de juiste aanpak is verschilt wederom per situatie en per analyse.
Data cleaning stap 3: welke data zijn onnodig?
Een volgende stap in Python data cleaning kan zijn om te kijken welke data onnodig zijn. Wederom kun je dit op verschillende manieren doen. Zo kun je bijvoorbeeld onderzoeken of:
- Er rijen zijn met bijna alleen maar dezelfde waarde. Als er nauwelijks variatie in een feature zit neemt logischerwijs ook de voorspellende waarde af.
- Er irrelevante features zijn opgenomen in de dataset. Zo zouden we in onze dataset over hotelboekingen niet zo veel hebben aan het aantal opa's en oma's van de kinderen die nog leven. Irrelevante features kunnen verwijderd worden.
- Er dubbelingen in de dataset voorkomen. Veel datasets hebben een unieke feature (bijvoorbeeld klant id) waardoor je gemakkelijk op basis van één column kunt ontdubbelen. Voor de dataset in deze tutorial hebben we niet zo'n feature en is dit dus lastiger.
Data cleaning stap 4: zijn er inconsistenties?
Tenslotte is het altijd goed om jezelf tijdens data cleaning af te vragen of er zich inconsistenties bevinden in de dataset. Verschillende veelvoorkomende inconsistenties zijn:
- Python is hoofdlettergevoelig en daarom kunnen inconsistenties in het gebruik van hoofdletters problemen veroorzaken. Dit kan makkelijk opgelost worden met .str.lower() voor features die uit tekst bestaan.
- Columns die data of tijden bevatten zijn dikwijls opgeslagen als data type 'string'. Deze kunnen het best omgezet worden naar DateTime format met pd.to_datetime()
- Binnen categorische variabelen kunnen typfouten voorkomen als de input niet gestandaardiseerd is. Er zijn verschillende manieren om typfouten te ontdekken. Het meest logisch is om te kijken welke entries erg op elkaar lijken, dit kan met Fuzzy logic.
Conclusie
Data cleaning in Python kost veel tijd, maar is essentieel voor iedereen die tot waardevolle inzichten wilt komen. Veelal start je met het identificeren van missende waarden en outliers. Als je observaties waarin dit voorkomt onder handen hebt genomen kun je verder duiken in het nut van de opgenomen features en de consistentie van de features die je wilt behouden. Zo kom je tot kwalitatieve input voor jouw modellen.
Wil je nog veel meer leren over Python en de mogelijkheden voor data analyse? Schrijf je dan in voor onze Python cursus voor data science, onze machine learning training, of voor onze data science opleiding en leer met vertrouwen te programmeren en analyseren in Python. Nadat je een van onze trainingen hebt gevolgd kun je zelfstandig verder aan de slag. Je kunt ook altijd even contact opnemen als je een vraag hebt.
Download één van onze opleidingsbrochures voor meer informatie
Rik is data scientist en marketeer bij Data Science Partners. Vanuit zijn achtergrond op de Technische Universiteit Eindhoven heeft hij veel affiniteit met data. Na zijn studie heeft hij als consultant altijd met data gewerkt en tevens ervaring opgedaan in het geven van trainingen.