Een uitgebreide handleiding voor het optimaliseren van Pandas DataFrames voor geheugengebruik en prestaties, inclusief datatypes, indexering en geavanceerde technieken.
Pandas DataFrame Optimalisatie: Geheugengebruik en Performance Tuning
Pandas is een krachtige Python bibliotheek voor datamanipulatie en analyse. Echter, bij het werken met grote datasets, kunnen Pandas DataFrames een aanzienlijke hoeveelheid geheugen verbruiken en trage prestaties vertonen. Dit artikel biedt een uitgebreide handleiding voor het optimaliseren van Pandas DataFrames voor zowel geheugengebruik als prestaties, waardoor u grotere datasets efficiƫnter kunt verwerken.
Inzicht in geheugengebruik in Pandas DataFrames
Voordat we ingaan op optimalisatietechnieken, is het cruciaal om te begrijpen hoe Pandas DataFrames data in het geheugen opslaan. Elke kolom in een DataFrame heeft een specifiek datatype, dat de hoeveelheid geheugen bepaalt die nodig is om de waarden op te slaan. Veel voorkomende datatypes zijn:
- int64: 64-bit integers (standaard voor integers)
- float64: 64-bit floating-point numbers (standaard voor floating-point numbers)
- object: Python objecten (gebruikt voor strings en gemengde datatypes)
- category: Categorische data (efficiƫnt voor repetitieve waarden)
- bool: Boolean waarden (True/False)
- datetime64: Datetime waarden
Het object datatype is vaak het meest geheugenintensief omdat het pointers naar Python objecten opslaat, die aanzienlijk groter kunnen zijn dan primitieve datatypes zoals integers of floats. Strings, zelfs korte, verbruiken, wanneer opgeslagen als `object`, veel meer geheugen dan nodig is. Evenzo is het gebruik van `int64` als `int32` voldoende zou zijn, verspilling van geheugen.
Voorbeeld: Inspecteren van DataFrame Geheugengebruik
U kunt de memory_usage() methode gebruiken om het geheugengebruik van een DataFrame te inspecteren:
import pandas as pd
import numpy as np
data = {
'col1': np.random.randint(0, 1000, 100000),
'col2': np.random.rand(100000),
'col3': ['A', 'B', 'C'] * (100000 // 3 + 1)[:100000],
'col4': ['This is a long string'] * 100000
}
df = pd.DataFrame(data)
memory_usage = df.memory_usage(deep=True)
print(memory_usage)
print(df.dtypes)
Het deep=True argument zorgt ervoor dat het geheugengebruik van objecten (zoals strings) nauwkeurig wordt berekend. Zonder `deep=True` wordt alleen het geheugen voor de pointers berekend, niet de onderliggende data.
Optimaliseren van Datatypes
Een van de meest effectieve manieren om het geheugengebruik te verminderen, is door de meest geschikte datatypes voor uw DataFrame kolommen te kiezen. Hier zijn enkele veel voorkomende technieken:
1. Downcasten van Numerieke Datatypes
Als uw integer of floating-point kolommen niet het volledige bereik van 64-bit precisie vereisen, kunt u ze downcasten naar kleinere datatypes zoals int32, int16, float32, of float16. Dit kan het geheugengebruik aanzienlijk verminderen, vooral voor grote datasets.
Voorbeeld: Beschouw een kolom die de leeftijd vertegenwoordigt, die waarschijnlijk niet hoger zal zijn dan 120. Het opslaan hiervan als `int64` is verspillend; `int8` (bereik -128 tot 127) zou meer geschikt zijn.
def downcast_numeric(df):
"""Downcasts numeric columns to the smallest possible data type."""
for col in df.columns:
if pd.api.types.is_integer_dtype(df[col]):
df[col] = pd.to_numeric(df[col], downcast='integer')
elif pd.api.types.is_float_dtype(df[col]):
df[col] = pd.to_numeric(df[col], downcast='float')
return df
df = downcast_numeric(df.copy())
print(df.memory_usage(deep=True))
print(df.dtypes)
De pd.to_numeric() functie met het downcast argument wordt gebruikt om automatisch het kleinst mogelijke datatype te selecteren dat de waarden in de kolom kan vertegenwoordigen. De `copy()` vermijdt het wijzigen van het originele DataFrame. Controleer altijd het bereik van waarden in uw data voordat u gaat downcasten om ervoor te zorgen dat u geen informatie verliest.
2. Gebruik van Categorische Datatypes
Als een kolom een beperkt aantal unieke waarden bevat, kunt u deze converteren naar een category datatype. Categorische datatypes slaan elke unieke waarde slechts ƩƩn keer op, en gebruiken vervolgens integer codes om de waarden in de kolom weer te geven. Dit kan het geheugengebruik aanzienlijk verminderen, vooral voor kolommen met een hoog percentage herhaalde waarden.
Voorbeeld: Beschouw een kolom die landcodes vertegenwoordigt. Als u te maken heeft met een beperkte set landen (bijv. alleen landen in de Europese Unie), is het opslaan hiervan als een categorie veel efficiƫnter dan het opslaan als strings.
def optimize_categories(df):
"""Converts object columns with low cardinality to categorical type."""
for col in df.columns:
if df[col].dtype == 'object':
num_unique_values = len(df[col].unique())
num_total_values = len(df[col])
if num_unique_values / num_total_values < 0.5:
df[col] = df[col].astype('category')
return df
df = optimize_categories(df.copy())
print(df.memory_usage(deep=True))
print(df.dtypes)
Deze code controleert of het aantal unieke waarden in een object kolom minder is dan 50% van de totale waarden. Zo ja, dan converteert het de kolom naar een categorisch datatype. De 50% drempel is arbitrair en kan worden aangepast op basis van de specifieke kenmerken van uw data. Deze aanpak is het meest gunstig wanneer de kolom veel herhaalde waarden bevat.
3. Vermijden van Object Datatypes voor Strings
Zoals eerder vermeld, is het object datatype vaak het meest geheugenintensief, vooral wanneer het wordt gebruikt om strings op te slaan. Probeer indien mogelijk het gebruik van object datatypes voor string kolommen te vermijden. Categorische types hebben de voorkeur voor strings met lage cardinaliteit. Als de cardinaliteit hoog is, overweeg dan of de strings kunnen worden weergegeven met numerieke codes of dat de string data helemaal kan worden vermeden.
Als u stringbewerkingen op de kolom moet uitvoeren, moet u deze mogelijk als een objecttype bewaren, maar overweeg of deze bewerkingen upfront kunnen worden uitgevoerd en vervolgens kunnen worden geconverteerd naar een efficiƫnter type.
4. Datum en Tijd Data
Gebruik het `datetime64` datatype voor datum- en tijdinformatie. Zorg ervoor dat de resolutie geschikt is (nanoseconde resolutie is mogelijk onnodig). Pandas verwerkt tijdreeksdata zeer efficiƫnt.
Optimaliseren van DataFrame Bewerkingen
Naast het optimaliseren van datatypes, kunt u ook de prestaties van Pandas DataFrames verbeteren door de bewerkingen die u erop uitvoert te optimaliseren. Hier zijn enkele veel voorkomende technieken:
1. Vectorisatie
Vectorisatie is het proces van het uitvoeren van bewerkingen op hele arrays of kolommen tegelijk, in plaats van het itereren over individuele elementen. Pandas is sterk geoptimaliseerd voor gevectoriseerde bewerkingen, dus het gebruik ervan kan de prestaties aanzienlijk verbeteren. Vermijd expliciete loops waar mogelijk. De ingebouwde functies van Pandas zijn over het algemeen veel sneller dan equivalente Python loops.
Voorbeeld: In plaats van door een kolom te itereren om het kwadraat van elke waarde te berekenen, gebruikt u de pow() functie:
# Inefficiƫnt (met behulp van een loop)
import time
start_time = time.time()
results = []
for value in df['col2']:
results.append(value ** 2)
df['col2_squared_loop'] = results
end_time = time.time()
print(f"Loop time: {end_time - start_time:.4f} seconds")
# Efficiƫnt (met behulp van vectorisatie)
start_time = time.time()
df['col2_squared_vectorized'] = df['col2'] ** 2
end_time = time.time()
print(f"Vectorized time: {end_time - start_time:.4f} seconds")
De gevectoriseerde aanpak is doorgaans ordes van grootte sneller dan de loop-gebaseerde aanpak.
2. Gebruik van `apply()` met Voorzichtigheid
De apply() methode stelt u in staat om een functie toe te passen op elke rij of kolom van een DataFrame. Het is echter over het algemeen langzamer dan gevectoriseerde bewerkingen omdat het het aanroepen van een Python functie voor elk element omvat. Gebruik apply() alleen wanneer gevectoriseerde bewerkingen niet mogelijk zijn.
Als u `apply()` moet gebruiken, probeer dan de functie die u toepast zoveel mogelijk te vectoriseren. Overweeg het gebruik van Numba's `jit` decorator om de functie te compileren naar machinecode voor aanzienlijke prestatieverbeteringen.
from numba import jit
@jit(nopython=True)
def my_function(x):
return x * 2 # Voorbeeldfunctie
df['col2_applied'] = df['col2'].apply(my_function)
3. Kolommen Efficiƫnt Selecteren
Gebruik de volgende methoden voor optimale prestaties bij het selecteren van een subset van kolommen uit een DataFrame:
- Directe kolomselectie:
df[['col1', 'col2']](snelste voor het selecteren van een paar kolommen) - Boolean indexering:
df.loc[:, [True if col.startswith('col') else False for col in df.columns]](handig voor het selecteren van kolommen op basis van een voorwaarde)
Vermijd het gebruik van df.filter() met reguliere expressies voor het selecteren van kolommen, omdat dit langzamer kan zijn dan andere methoden.
4. Optimaliseren van Joins en Merges
Het samenvoegen en samenvoegen van DataFrames kan rekenkundig duur zijn, vooral voor grote datasets. Hier zijn enkele tips voor het optimaliseren van joins en merges:
- Gebruik geschikte join keys: Zorg ervoor dat de join keys hetzelfde datatype hebben en geĆÆndexeerd zijn.
- Specificeer het join type: Gebruik het juiste join type (bijv.
inner,left,right,outer) op basis van uw vereisten. Een inner join is over het algemeen sneller dan een outer join. - Gebruik `merge()` in plaats van `join()`: De
merge()functie is veelzijdiger en vaak sneller dan dejoin()methode.
Voorbeeld:
df1 = pd.DataFrame({'key': ['A', 'B', 'C', 'D'], 'value1': [1, 2, 3, 4]})
df2 = pd.DataFrame({'key': ['B', 'D', 'E', 'F'], 'value2': [5, 6, 7, 8]})
# Efficiƫnte inner join
df_merged = pd.merge(df1, df2, on='key', how='inner')
print(df_merged)
5. Vermijden van het Onnodig Kopiƫren van DataFrames
Veel Pandas bewerkingen creƫren kopieƫn van DataFrames, die geheugenintensief en tijdrovend kunnen zijn. Om onnodig kopiƫren te voorkomen, gebruikt u het inplace=True argument indien beschikbaar, of wijst u het resultaat van een bewerking terug toe aan het originele DataFrame. Wees zeer voorzichtig met `inplace=True` omdat het fouten kan maskeren en het debuggen moeilijker kan maken. Het is vaak veiliger om opnieuw toe te wijzen, zelfs als het iets minder performant is.
Voorbeeld:
# Inefficiƫnt (creƫert een kopie)
df_filtered = df[df['col1'] > 500]
# Efficiƫnt (wijzigt het originele DataFrame in place - VOORZICHTIG)
df.drop(df[df['col1'] <= 500].index, inplace=True)
#VEILIGER - wijst opnieuw toe, vermijdt inplace
df = df[df['col1'] > 500]
6. Chunking en Itereren
Voor extreem grote datasets die niet in het geheugen passen, overweeg dan om de data in chunks te verwerken. Gebruik de `chunksize` parameter bij het lezen van data uit bestanden. Itereer door de chunks en voer uw analyse afzonderlijk uit op elke chunk. Dit vereist een zorgvuldige planning om ervoor te zorgen dat de analyse correct blijft, omdat sommige bewerkingen vereisen dat de hele dataset tegelijk wordt verwerkt.
# Lees CSV in chunks
for chunk in pd.read_csv('large_data.csv', chunksize=100000):
# Verwerk elke chunk
print(chunk.shape)
7. Gebruik van Dask voor Parallelle Verwerking
Dask is een parallelle computing bibliotheek die naadloos integreert met Pandas. Hiermee kunt u grote DataFrames parallel verwerken, wat de prestaties aanzienlijk kan verbeteren. Dask verdeelt het DataFrame in kleinere partities en verdeelt ze over meerdere cores of machines.
import dask.dataframe as dd
# Maak een Dask DataFrame
ddf = dd.read_csv('large_data.csv')
# Voer bewerkingen uit op het Dask DataFrame
ddf_filtered = ddf[ddf['col1'] > 500]
# Bereken het resultaat (dit triggert de parallelle berekening)
result = ddf_filtered.compute()
print(result.head())
Indexeren voor Snellere Lookups
Het creƫren van een index op een kolom kan lookups en filterbewerkingen aanzienlijk versnellen. Pandas gebruikt indexen om snel rijen te vinden die overeenkomen met een specifieke waarde.
Voorbeeld:
# Stel 'col3' in als de index
df = df.set_index('col3')
# Snellere lookup
value = df.loc['A']
print(value)
# Reset de index
df = df.reset_index()
Het creƫren van te veel indexen kan echter het geheugengebruik verhogen en schrijfbewerkingen vertragen. Maak alleen indexen op kolommen die vaak worden gebruikt voor lookups of filtering.
Andere Overwegingen
- Hardware: Overweeg om uw hardware (CPU, RAM, SSD) te upgraden als u consistent met grote datasets werkt.
- Software: Zorg ervoor dat u de nieuwste versie van Pandas gebruikt, omdat nieuwere versies vaak prestatieverbeteringen bevatten.
- Profiling: Gebruik profiling tools (bijv.
cProfile,line_profiler) om prestatieknelpunten in uw code te identificeren. - Data Storage Format: Overweeg het gebruik van efficiƫntere dataopslagformaten zoals Parquet of Feather in plaats van CSV. Deze formaten zijn kolomsgewijs en vaak gecomprimeerd, wat leidt tot kleinere bestandsgroottes en snellere lees-/schrijftijden.
Conclusie
Het optimaliseren van Pandas DataFrames voor geheugengebruik en prestaties is cruciaal voor het efficiƫnt werken met grote datasets. Door de juiste datatypes te kiezen, gevectoriseerde bewerkingen te gebruiken en uw data effectief te indexeren, kunt u het geheugengebruik aanzienlijk verminderen en de prestaties verbeteren. Vergeet niet om uw code te profilen om prestatieknelpunten te identificeren en overweeg het gebruik van chunking of Dask voor extreem grote datasets. Door deze technieken te implementeren, kunt u het volledige potentieel van Pandas voor data-analyse en -manipulatie ontsluiten.