AIニュース最前線
最新ニュースAI日報Hacker日報週報動画AIツールトレンド企業

AIニュース最前線

世界中のAI最新情報を日本語で毎時更新

最新ニュース日報トレンド企業プレミアムRSS
© 2026 ainew.jp特定商取引法に基づく表記
ニュース一覧元記事を開く
KDnuggets·2026年6月15日 21:00·約13分で読める

データクリーニングと前処理のための Pandas の 3 つの技

#データ前処理#Pandas#データクリーニング#Python#機械学習パイプライン
TL;DR

KDnuggets が紹介する記事は、Pandas ライブラリを活用してデータクリーニングと前処理の効率を劇的に向上させる 3 つの実用的なテクニックを解説している。

AI深層分析2026年6月15日 22:02
3
注目/ 5段階
深度40%
4
関連度30%
4
実用性20%
5
革新性10%
2

キーポイント

1

効率的なデータクリーニング手法

重複値や欠損値の処理において、従来のループ処理ではなくベクトル化された Pandas メソッドを活用することで、コードの可読性と実行速度を向上させる。

2

前処理パイプラインの構築

複数のデータ変換ステップを関数として定義し、Pipeline 構造で連結することで、再現性の高い一貫したデータ前処理ワークフローを実現する。

3

複雑な条件付けと結合の簡素化

多次元のデータ結合や複雑な条件フィルタリングにおいて、Pandas の高度な機能(例:merge, applymap)を組み合わせることで、冗長なコードを削減する。

影響分析・編集コメントを表示

影響分析

この記事は、データサイエンティストやエンジニアが日常的に直面する非効率なコード改善の具体的な解決策を提供し、実務における生産性向上に直結します。特に大規模データを扱う現場において、Pandas の深い理解に基づいた最適化手法を共有することで、業界全体のコーディング標準の底上げに寄与すると考えられます。

編集コメント

本記事は特定の AI モデル開発に関する最新動向ではありませんが、AI/ML プロジェクトの基盤となるデータ整備工程において、即座に実践可能なスキル向上を促す貴重な技術解説です。

imageimage**

# イントロダクション

データクリーニングと準備には、データサイエンティストの日常業務の最大 80% が費やされると推定されています。Pandas は Python における標準的なデータ操作ライブラリであるため、あなたの処理効率がいかに速く生データ(raw data)からモデル準備済みの特徴量へと移行できるかを直接決定します。クリーニングと準備に時間を割きたいという理由には明確な根拠があります:それは直接的にモデリング、分析、そしてインサイトの伝達に費やすことのできる時間の増加につながります。

しかし、多くの開発者は標準的な Python のループ構造を模倣した Pandas コードを書いたり、命令型で状態を変更する更新処理を使用したりしています。これらのアプローチにはいくつかの問題があります:混乱を招く SettingWithCopyWarning を引き起こす可能性があり、冗長なコピーによって RAM 使用量が肥大化し、ベクトル化を回避することで実行速度が低下します。

本番環境向けのデータパイプラインを作成するには、基本構文から Idiomatic Pandas(Pandas の慣習的な)デザインパターンへと移行する必要があります。この記事では、データを効率的にクリーニングし準備するための 3 つの重要な Pandas トリックを紹介します:

  • 宣言的メソッドチェーン
  • カテゴリカルデータとベクトル化された文字列アクセサによるメモリおよび速度の最適化
  • .transform() を用いたグループ認識型インプット(欠損値補完)

データを準備する際、文字列値のクリーニング、新しい数式カラムの作成、外れ値のフィルタリング、フィールド名の変更などの一連の変換処理を行うことが一般的です。

これらの操作を逐次的に記述し、データフレームをその場で変更するか、同じ変数へ繰り返し再代入するといった素朴なアプローチは、コードが読みづらくデバッグも困難になるだけでなく、スライスされたデータフレームを変更する際に有名な SettingWithCopyWarning を頻繁に引き起こします。この警告は、メモリ上のコピーか元の配列バッファのどちらを修正しているかを Pandas が保証できないことを示しています。

データクリーニングパイプラインを括弧で囲むことで、Pandas のメソッドを順次チェーンできます。.assign() を使用して新しいカラムを宣言し、.query() で行のフィルタリングを行い、.pipe() でカスタム関数を適用することで、操作を線形かつ可読性の高いものにし、副作用から守ることができます。

この命令型スタイルはデータフレームを段階的に変更するため、警告アラートのリスクがあり、中間段階を特定することが困難になります:

import pandas as pd

import numpy as np

サンプルの生販売データ

data = {

'sale_date': ['2026-01-01', '2026-01-02', 'invalid_date', '2026-01-04'],

'item_code': [' PROD_A ', ' PROD_B', 'PROD_C ', ' PROD_D '],

'price': [100.0, 250.0, -99.0, 150.0],

'quantity': [2, 1, 5, 3]

}

df = pd.DataFrame(data)

Naive multi-step cleaning

df['sale_date'] = pd.to_datetime(df['sale_date'], errors='coerce')

df['item_code'] = df['item_code'].str.strip()

df['total_revenue'] = df['price'] * df['quantity']

Filtering out bad dates and invalid prices

df = df[df['sale_date'].notna()]

df = df[df['price'] > 0]

Renaming columns for consistency

df.rename(columns={'item_code': 'product_id'}, inplace=True)

print(df)

ここでは、全く同じロジックを単一の、まとまりのあるトップダウンのパイプラインに再構築します。カスタムヘルパー関数を .pipe() と共に使用して、独自のアノマリー(異常値)に対応します:

import pandas as pd

import numpy as np

data = {

'sale_date': ['2026-01-01', '2026-01-02', 'invalid_date', '2026-01-04'],

'item_code': [' PROD_A ', ' PROD_B', 'PROD_C ', ' PROD_D '],

'price': [100.0, 250.0, -99.0, 150.0],

'quantity': [2, 1, 5, 3]

}

df_raw = pd.DataFrame(data)

Custom modular cleaning step

def clean_item_codes(df):

df['item_code'] = df['item_code'].str.strip()

return df

Method Chaining pipeline

cleaned_df = (

df_raw

.copy() # Prevents modifying the original raw data

.assign(

sale_date=lambda d: pd.to_datetime(d['sale_date'], errors='coerce'),

total_revenue=lambda d: d['price'] * d['quantity']

)

.pipe(clean_item_codes)

.query("sale_date.notna() and price > 0")

.rename(columns={'item_code': 'product_id'})

)

print(cleaned_df)

Output:

sale_date product_id price quantity total_revenue

0 2026-01-01 PROD_A 100.0 2 200.0

1 2026-01-02 PROD_B 250.0 1 250.0

3 2026-01-04 PROD_D 150.0 3 450.0

式を ( ... ) で囲むことで、Python はバックスラッシュを使用せずに複数行のチェーンを可能にします。

  • .assign() はキーワード引数を受け取り、ラムダ関数が DataFrame の現在の状態 (d) を受け取るため、複数の列を順次作成または変更できます。
  • .pipe() は中間の DataFrame を外部関数に渡します。これにより、再利用可能なクリーニングロジックをメインのチェーンから分離できます。
  • .query() は文字列としてブール式を受け取ります。これはネストされた括弧 (df[(df[a] > 0) & (df[b].notna())]) よりも清潔で、内部では NumPy の高速数値式評価器である NumExpr を使用して実行速度が向上します。

この関数型パターンは、中間のスライスを決して変更しないため、SettingWithCopyWarning を回避できます。

# 2. カテゴリカルデータとベクトル化文字列メソッドによるメモリおよび速度の最適化

デフォルトでは、Pandas はテキストを含む列に汎用のオブジェクトデータ型を割り当てます。オブジェクト列は、ヒープメモリ上に散在する文字列への Python ポインタを格納しており、連続してパッキングされた値ではありません。低カードinality(カテゴリの重複が多い)な文字列を持つ大規模データセット(ステータスフラグ、都市名、性別など、繰り返しのあるカテゴリを含む列)の場合、このデフォルト設定は明らかなメモリ使用量をもたらします。

さらに、開発者は Python のラムダ式を .apply() に渡すことで、カスタム文字列変換を頻繁に適用します。これにより、Pandas は低速な Python インタープリタ速度で各行を順次ループ処理する必要があります。

RAM 使用量と実行時間の両方を最適化するには、以下の方法があります:

  • 低カルディナリティ(値の種類の少なさ)を持つ文字列カラムをネイティブのカテゴリデータ型に変換する
  • 低速な .apply() ループを、.str アクセサを介した最適化されたベクトル化文字列メソッドに置き換える

テキストをオブジェクトカラムとして保持し、.apply() を使用して空白文字をクリーニングする大規模データセット(100 万行)のクリーニングをシミュレーションしてみましょう:

import pandas as pd

import numpy as np

import time

カードinalityの低い文字列データを 100 万行含むモックデータセットを作成

n_rows = 1000000

categories = [' PENDING ', ' COMPLETED ', ' FAILED ', ' SHIPPED ']

df = pd.DataFrame({

'status': np.random.choice(categories, size=n_rows),

'val': np.random.rand(n_rows)

})

クリーニング前のメモリ使用量をベンチマーク

mem_before = df['status'].memory_usage(deep=True) / (1024 ** 2)

start_time = time.time()

素朴なクリーニング:低速な Python の apply ループ

df['status'] = df['status'].apply(lambda x: x.strip().upper())

duration_apply = time.time() - start_time

mem_after = df['status'].memory_usage(deep=True) / (1024 ** 2)

print(f"Apply クリーニング完了までの時間:{duration_apply:.4f} 秒")

print(f"Status カラムのメモリ使用量:{mem_after:.2f} MB(元々は {mem_before:.2f} MB)")

status カラムをまずカテゴリ型に変換し、ベクトル化された .str アクセサーを使用することで、即座に速度向上を実現し、大幅なメモリ節約が可能になります:

import pandas as pd

import numpy as np

import time

n_rows = 1000000

categories = [' PENDING ', ' COMPLETED ', ' FAILED ', ' SHIPPED ']

df = pd.DataFrame({

'status': np.random.choice(categories, size=n_rows),

'val': np.random.rand(n_rows)

})

カテゴリ型への変換

df['status'] = df['status'].astype('category')

メモリ使用量のベンチマーク

mem_category = df['status'].memory_usage(deep=True) / (1024 ** 2)

start_time = time.time()

カテゴリに対して直接ベクトル化された文字列クリーニング

df['status'] = df['status'].cat.rename_categories(lambda x: x.strip().upper())

duration_vectorized = time.time() - start_time

print(f"Vectorized category cleaning completed in: {duration_vectorized:.4f} seconds")

print(f"Category status column memory usage: {mem_category:.2f} MB")

print(f"Speedup: {duration_apply / duration_vectorized:.2f}x faster")

Combined output:

Apply cleaning completed in: 0.1213 seconds

Status column memory usage: 53.64 MB (originally 55.55 MB)

Vectorized category cleaning completed in: 0.0003 seconds

Category status column memory usage: 0.95 MB

Speedup: 407.83x faster

これらのパフォーマンス向上は、間違いなく勝利と言えます。

カラムをカテゴリ型に変換すると、Pandas は内部で文字列を整数キーにエンコードします(例:PENDING -> 0, COMPLETED -> 1)。

  • 100 万個の文字列を保存する代わりに、Pandas は 100 万個の小さな整数と、4 つの実際の文字列カテゴリからなる非常に小さなマッピング(map)を保存します。これにより、メモリ使用量は約 56 MB から 1 MB 未満に削減されます。
  • .cat.rename_categories() を用いてラベルを直接クリーニングすることで、Pandas は文字列操作を 100 万行すべてをループするのではなく、4 つの一意のカテゴリに対してのみ実行します。これにより、実行時間がほぼゼロまで短縮されます。

注記:**高カルディナリティ(high-cardinality)のテキスト(値がほとんど重複しない場合)を扱っている場合は、カテゴリ型として保持してもメモリ節約にはなりません。そのようなケースでは、.apply() を使用せず、オブジェクト列に対してベクトル化された文字列メソッドを直接使用する必要があります:df['status'].str.strip().str.upper()。これは Python ではなくコンパイルされた C で実行されるため高速です。

# 3. groupby() と .transform() を用いたグループ認識型インプテーションと補間

**

欠損値の処理はデータクリーニングにおける基本的なステップです。多くの場合、欠損値をグローバル平均や定数で置き換えることは統計的バイアスを導入します。例えば、欠損した製品価格をインプートする際、全店舗製品のグローバル平均価格を使用するのは不正確です。その特定の製品カテゴリの平均価格を用いてインプートする方がはるかに精度が高くなります。

素朴なアプローチは、製品カテゴリをループしてグループ平均を計算し、DataFrame をフィルタリングし、欠損値を補充し、最後にグループを再接続することです。あるいは、groupby().apply() 内でカスタム関数を使用すると、スケーラビリティの悪い分割・適用・結合サイクルが発生し、パフォーマンスが低下します。

最適化された解決策は、groupby() と .transform() メソッドを組み合わせて使用することです。

ここでは、ループまたは .apply() に渡されるカスタム関数を使用して、欠損する数値価格(NaN で表される)を補完するシミュレーションを行います:

import pandas as pd

import numpy as np

import time

カテゴリごとにグループ化された 100,000 個のアイテムのモックカタログを作成

n_items = 100000

categories = [f"CAT_{i}" for i in range(100)]

df = pd.DataFrame({

'category': np.random.choice(categories, size=n_items),

'price': np.random.uniform(10.0, 500.0, size=n_items)

})

価格の 10% を欠損(NaN)として導入

nan_mask = np.random.rand(n_items) < 0.1

df.loc[nan_mask, 'price'] = np.nan

df_clunky = df.copy()

start_time = time.time()

カスタム lambda を使用した分割・適用・結合(apply())

df_clunky['price'] = df_clunky.groupby('category')['price'].apply(lambda x: x.fillna(x.mean())).reset_index(level=0, drop=True)

duration_clunky = time.time() - start_time

print(f"Apply ベースのグループ補完にかかった時間:{duration_clunky:.4f} 秒")

.transform() を活用することで、カスタム lambda ループを回避し、Pandas にインデックス整合性とベクトル化をネイティブに処理させることができます:

import pandas as pd

import numpy as np

import time

同じセットアップを使用する

df_optimized = df.copy()

start_time = time.time()

transform を使用した最適化アプローチ

group_means = df_optimized.groupby('category')['price'].transform('mean')

df_optimized['price'] = df_optimized['price'].fillna(group_means)

duration_opt = time.time() - start_time

print(f"Transform ベースのグループ補完にかかった時間: {duration_opt:.4f} 秒")

print(f"速度向上: {duration_clunky / duration_opt:.2f}倍高速")

出力:

Apply ベースのグループ補完にかかった時間: 0.0224 秒

Transform ベースのグループ補完にかかった時間: 0.0032 秒

速度向上: 7.04 倍高速

.transform() の動作を理解することが、高性能な Pandas コードを書く鍵となります:

  • df.groupby('category')['price'].transform('mean') を実行すると、Pandas は各カテゴリの平均価格を計算します。
  • より小さなグループ化された集計テーブルを返すのではなく、.transform() は計算された値を元の DataFrame のサイズと整合性に合わせてブロードキャスト(再配置)します。結果として出力されるのは、元のデータセットと同じ長さを持つ Series で、インデックス i には行 i が属するグループの平均が含まれます。
  • その後、df['price'].fillna(group_means) を使用できます。これにより、クリーンでベクトル化され、インデックスが整合した代入によって欠損値を埋めることができます。

このパターンは非常に汎用性が高く、グループレベルでの標準化(例:グループ平均の減算)や、df.groupby('group')['val'].transform('ffill') を使用して各グループ内で前方補完を行うことにも利用できます。

まとめ

基本的な単純なループ構造を超え、慣用的な Pandas の設計パターンを採用することで、ローカルプロトタイプから本番環境までシームレスにスケーリングするデータ準備パイプラインを構築できます。

振り返りましょう:

  • メソッドチェーンは、脆く多行の命令形変異を置き換え、SettingWithCopyWarning を完全に回避する読みやすい宣言的処理シーケンスになります
  • カテゴリカルキャストとベクトル化文字列メソッドはメモリレイアウトを最適化し、文字列変換を C スピードの実行にオフロードすることで、低カードinalityデータで RAM 使用量を最大 98% 削減します
  • .transform() を用いたグループ認識型インプットは、グループレベルの統計量を計算し、それらをネイティブで元のインデックス形状に戻すことで、低速なカスタムグルーピングループを回避します

これらのパターンを日常業務に取り入れることで、特徴量エンジニアリングとデータクリーニングのプロセスが高速化され、クリーンで保守性の高いものになります。

Matthew Mayo** (@mattmayo13) は、コンピュータサイエンスの修士号とデータマイニングの大学院ディプロマを保有しています。KDnuggets と Statology の編集長、および Machine Learning Mastery の寄稿編集者として、Matthew は複雑なデータサイエンスの概念を誰もが理解できるようにすることを目的としています。彼の専門的な関心分野には、自然言語処理(Natural Language Processing)、言語モデル、機械学習アルゴリズム、そして新興する AI の探求が含まれます。彼はデータサイエンスコミュニティにおける知識の民主化という使命に駆り立てられています。Matthew は 6 歳の頃からコーディングを続けています。

原文を表示
3 Pandas Tricks for Data Cleaning & Preparation
3 Pandas Tricks for Data Cleaning & Preparation

**

# Introduction

Data cleaning and preparation are estimated to occupy up to 80% of a data scientist's daily workflow. Because Pandas is the standard data manipulation library in Python, the efficiency of your operations directly dictates how quickly you can move from raw, dirty datasets to model-ready features. And there is good reason to want to increase your cleaning and preparation time: it translates directly to more time available to spend on modeling, analysis, and communicating insights.

However, many developers write Pandas code that mimics standard Python looping structures or uses imperative, state-mutating updates. These approaches suffer from several issues: they can trigger the confusing SettingWithCopyWarning, bloat RAM usage with redundant copies, and drag execution speed down by avoiding vectorization.

To write production-grade data pipelines, you need to transition from basic syntax to idiomatic Pandas design patterns. In this article, we will walk through three essential Pandas tricks to clean and prepare your data efficiently:

  • declarative method chaining
  • memory and speed optimization via categoricals and vectorized string accessors
  • group-aware imputation using .transform()

# 1. Declarative Method Chaining with .assign(), .query(), and .pipe()

When preparing data, it is common to perform a sequence of modifications: cleaning string values, creating new mathematical columns, filtering outliers, renaming fields, and so on.

A naive approach writes these operations sequentially, mutating the DataFrame in-place or reassigning it to the same variable repeatedly. Not only does this make code hard to read and debug, but modifying sliced DataFrames also frequently triggers the infamous [SettingWithCopyWarning](https://pandas.pydata.org/pandas-docs/version/2.2/reference/api/pandas.errors.SettingWithCopyWarning.html). This warning is Pandas telling you that it cannot guarantee whether you are modifying a copy or the original array buffer in memory.

By wrapping your data cleaning pipeline in parentheses, you can chain Pandas methods sequentially. Using .assign() to declare new columns, .query() for row filtering, and .pipe() to apply custom functions keeps your operations linear, readable, and safe from side-effects.

This imperative style modifies the DataFrame step-by-step, running the risk of warning alerts and making intermediate stages hard to isolate:

code
import pandas as pd
import numpy as np

# Sample raw sales data
data = {
    'sale_date': ['2026-01-01', '2026-01-02', 'invalid_date', '2026-01-04'],
    'item_code': ['  PROD_A ', ' PROD_B', 'PROD_C  ', '  PROD_D '],
    'price': [100.0, 250.0, -99.0, 150.0],
    'quantity': [2, 1, 5, 3]
}
df = pd.DataFrame(data)

# Naive multi-step cleaning
df['sale_date'] = pd.to_datetime(df['sale_date'], errors='coerce')
df['item_code'] = df['item_code'].str.strip()
df['total_revenue'] = df['price'] * df['quantity']

# Filtering out bad dates and invalid prices
df = df[df['sale_date'].notna()]
df = df[df['price'] > 0]

# Renaming columns for consistency
df.rename(columns={'item_code': 'product_id'}, inplace=True)

print(df)

Here, we restructure the exact same logic into a single, cohesive, top-to-bottom pipeline. We use a custom helper function with .pipe() to handle custom anomalies:

code
import pandas as pd
import numpy as np

data = {
    'sale_date': ['2026-01-01', '2026-01-02', 'invalid_date', '2026-01-04'],
    'item_code': ['  PROD_A ', ' PROD_B', 'PROD_C  ', '  PROD_D '],
    'price': [100.0, 250.0, -99.0, 150.0],
    'quantity': [2, 1, 5, 3]
}
df_raw = pd.DataFrame(data)

# Custom modular cleaning step
def clean_item_codes(df):
    df['item_code'] = df['item_code'].str.strip()
    return df

# Method Chaining pipeline
cleaned_df = (
    df_raw
    .copy()  # Prevents modifying the original raw data
    .assign(
        sale_date=lambda d: pd.to_datetime(d['sale_date'], errors='coerce'),
        total_revenue=lambda d: d['price'] * d['quantity']
    )
    .pipe(clean_item_codes)
    .query("sale_date.notna() and price > 0")
    .rename(columns={'item_code': 'product_id'})
)

print(cleaned_df)

Output:

code
   sale_date product_id  price  quantity  total_revenue
0 2026-01-01     PROD_A  100.0         2          200.0
1 2026-01-02     PROD_B  250.0         1          250.0
3 2026-01-04     PROD_D  150.0         3          450.0

By wrapping the expression in ( ... ), Python allows multi-line chains without using backslashes.

  • .assign() takes keyword arguments where lambdas receive the current state of the DataFrame (d), enabling you to create or modify multiple columns sequentially.
  • .pipe() passes the intermediate DataFrame to an external function. This separates reusable cleaning logic from the main chain.
  • .query() accepts a boolean expression as a string. It is cleaner than nested brackets (df[(df[a] > 0) & (df[b].notna())]) and runs faster under the hood using NumPy's fast numerical expression evaluator, NumExpr.

This functional pattern avoids SettingWithCopyWarning because it never modifies intermediate slices.

# 2. Memory & Speed Optimization with Categoricals and Vectorized String Methods

By default, Pandas assigns the generic object data type to columns containing text. An object column stores Python pointers to strings scattered in heap memory, rather than contiguous, packed values. For large datasets with low-cardinality strings (columns with repetitive categories, such as status flags, city names, or gender), this defaults to an obvious memory footprint.

Furthermore, developers frequently apply custom string modifications by passing Python lambda expressions to .apply(). This forces Pandas to loop sequentially over every row at slow Python interpreter speeds.

We can optimize both RAM usage and execution time by:

  • Converting low-cardinality string columns to the native category data type
  • Replacing slow .apply() loops with optimized vectorized string methods via the .str accessor

Let's simulate cleaning a large dataset (1,000,000 rows) by keeping text as object columns and cleaning whitespaces using .apply():

code
import pandas as pd
import numpy as np
import time

# Create a mock dataset with 1 million rows of low-cardinality string data
n_rows = 1000000
categories = [' PENDING ', ' COMPLETED ', ' FAILED ', ' SHIPPED ']
df = pd.DataFrame({
    'status': np.random.choice(categories, size=n_rows),
    'val': np.random.rand(n_rows)
})

# Benchmark memory usage before cleaning
mem_before = df['status'].memory_usage(deep=True) / (1024 ** 2)

start_time = time.time()

# Naive cleaning: slow Python apply loops
df['status'] = df['status'].apply(lambda x: x.strip().upper())
duration_apply = time.time() - start_time

mem_after = df['status'].memory_usage(deep=True) / (1024 ** 2)

print(f"Apply cleaning completed in: {duration_apply:.4f} seconds")
print(f"Status column memory usage: {mem_after:.2f} MB (originally {mem_before:.2f} MB)")

By casting the status column to category first, and using the vectorized .str accessor, we achieve instant speedups and save significant memory:

code
import pandas as pd
import numpy as np
import time

n_rows = 1000000
categories = [' PENDING ', ' COMPLETED ', ' FAILED ', ' SHIPPED ']
df = pd.DataFrame({
    'status': np.random.choice(categories, size=n_rows),
    'val': np.random.rand(n_rows)
})

# Convert to category dtype
df['status'] = df['status'].astype('category')

# Benchmark memory usage
mem_category = df['status'].memory_usage(deep=True) / (1024 ** 2)

start_time = time.time()

# Vectorized string cleaning directly on categories
df['status'] = df['status'].cat.rename_categories(lambda x: x.strip().upper())
duration_vectorized = time.time() - start_time

print(f"Vectorized category cleaning completed in: {duration_vectorized:.4f} seconds")
print(f"Category status column memory usage: {mem_category:.2f} MB")
print(f"Speedup: {duration_apply / duration_vectorized:.2f}x faster")

Combined output:

code
Apply cleaning completed in: 0.1213 seconds
Status column memory usage: 53.64 MB (originally 55.55 MB)

Vectorized category cleaning completed in: 0.0003 seconds
Category status column memory usage: 0.95 MB
Speedup: 407.83x faster

We'll call those performance improvements a win.

When a column is cast to category, Pandas encodes the strings to integer keys under the hood (e.g. PENDING -> 0, COMPLETED -> 1).

  • Instead of storing 1,000,000 strings, Pandas stores 1,000,000 small integers and a tiny map of 4 actual string categories. This reduces the memory footprint from ~56 MB to less than 1 MB.
  • By cleaning the labels directly using .cat.rename_categories(), Pandas only performs the string operations on the 4 unique categories rather than looping through 1,000,000 rows. The execution time drops to almost zero.

Note:** If you are working with high-cardinality text (where values rarely repeat), keeping it as category will not save memory. In those cases, you should still avoid .apply() and use vectorized string methods directly on the object column: df['status'].str.strip().str.upper(), which executes in compiled C rather than Python.

# 3. Group-Aware Imputation and Interpolation with groupby() and .transform()

**

Handling missing data is a fundamental step in data cleaning. In many cases, replacing missing values with a global average or constant introduces statistical bias. For example, if you are imputing a missing product price, using the global average price of all store products is inaccurate. It is much more precise to impute using the average price of that specific product category.

The naive approach is to loop over the product categories, calculate the group mean, filter the DataFrame, fill the missing values, and stitch the groups back together. Alternatively, using a custom function inside groupby().apply() triggers slow split-apply-combine cycles that scale poorly.

The optimized solution is to combine groupby() with the .transform() method.

Here, we simulate imputing missing numerical prices (represented by NaN) using a loop or a custom function passed to .apply():

code
import pandas as pd
import numpy as np
import time

# Create a mock catalog of 100,000 items grouped by category
n_items = 100000
categories = [f"CAT_{i}" for i in range(100)]

df = pd.DataFrame({
    'category': np.random.choice(categories, size=n_items),
    'price': np.random.uniform(10.0, 500.0, size=n_items)
})

# Introduce 10% missing prices (NaN)
nan_mask = np.random.rand(n_items) < 0.1
df.loc[nan_mask, 'price'] = np.nan

df_clunky = df.copy()

start_time = time.time()

# Split-apply-combine using apply() with a custom lambda
df_clunky['price'] = df_clunky.groupby('category')['price'].apply(lambda x: x.fillna(x.mean())).reset_index(level=0, drop=True)
duration_clunky = time.time() - start_time

print(f"Apply-based group imputation took: {duration_clunky:.4f} seconds")

By leveraging .transform(), we bypass custom lambda loops and allow Pandas to handle index alignment and vectorization natively:

code
import pandas as pd
import numpy as np
import time

# Use the same setup
df_optimized = df.copy()

start_time = time.time()

# Optimized approach using transform
group_means = df_optimized.groupby('category')['price'].transform('mean')
df_optimized['price'] = df_optimized['price'].fillna(group_means)
duration_opt = time.time() - start_time

print(f"Transform-based group imputation took: {duration_opt:.4f} seconds")
print(f"Speedup: {duration_clunky / duration_opt:.2f}x faster")

Output:

code
Apply-based group imputation took: 0.0224 seconds
Transform-based group imputation took: 0.0032 seconds
Speedup: 7.04x faster

Understanding how .transform() operates is key to writing high-performance Pandas code:

  • When you run df.groupby('category')['price'].transform('mean'), Pandas calculates the mean price for each category.
  • Instead of returning a smaller grouped summary table, .transform() broadcasts the calculated values back to the size and alignment of the original DataFrame. It outputs a series of the exact same length as the original dataset, where index i contains the mean of the group that row i belongs to.
  • We can then use df['price'].fillna(group_means). This fills the missing values using a clean, vectorized, index-aligned assignment.

This pattern is highly versatile. You can use it to perform group-level standardization (e.g. subtracting group means) or forward-fill missing values per group using: df.groupby('group')['val'].transform('ffill').

# Wrapping Up

By moving beyond basic, naive loop constructs and adopting idiomatic Pandas design patterns, you can build data preparation pipelines that scale seamlessly from local prototypes to production environments.

Let's recap:

  • Method chaining replaces brittle, multi-line imperative mutation with readable, declarative processing sequences that completely avoid SettingWithCopyWarning
  • Categorical casting & vectorized string methods optimize memory layouts and offload string transformations to C-speed execution, slashing RAM usage by up to 98% on low-cardinality data
  • Group-aware imputation with .transform() calculates group-level statistics and aligns them back to the original index shapes natively, avoiding slow custom grouping loops

Incorporating these patterns into your daily work will make your feature engineering and data cleaning processes fast, clean, and highly maintainable.

Matthew Mayo** (@mattmayo13) holds a master's degree in computer science and a graduate diploma in data mining. As managing editor of KDnuggets & Statology, and contributing editor at Machine Learning Mastery, Matthew aims to make complex data science concepts accessible. His professional interests include natural language processing, language models, machine learning algorithms, and exploring emerging AI. He is driven by a mission to democratize knowledge in the data science community. Matthew has been coding since he was 6 years old.

この記事をシェア

関連記事

KDnuggets★32026年5月27日 23:00

例題付きで解説するPandasのGroupBy機能

KDnuggetsが公開した記事は、Pythonのデータ分析ライブラリ「Pandas」におけるグループ化操作(GroupBy)の仕組みを具体的なコード例を用いて分かりやすく説明している。

KDnuggets★32026年6月16日 21:00

Pandas でループを書かない:試すべき 7 つの高速代替案

KDnuggets は、Pandas データ処理でループを使用する代わりに、ベクトル化や組み込み関数など 7 つの高速な代替手法を紹介している。

KDnuggets★32026年6月19日 21:00

データサイエンティストが知っておくべき実用的な SQL の技

KDnuggets は、データサイエンティストが効率的にデータを処理するために役立つ実践的な SQL のテクニックを紹介している。

今日のまとめ

AI日報で今日の重要ニュースをまとめ読み

ニュース一覧に戻る元記事を読む