Apply Custom Functions with vectorize

Apply custom functions with vectorize — learn how to use numpy.vectorize to apply Python functions element-wise on arrays. This Data Analysis with Python tutorial explains the concept, provides a hands-on exercise, and covers troubleshooting.

Focus: apply custom functions with vectorize

Sponsored

You've spent hours writing a for loop to apply a custom function to every element in a NumPy array, only to watch it crawl on a dataset with hundreds of thousands of rows. The loop is easy to read, but it's slow, verbose, and completely ignores the power of NumPy. There's a better way: numpy.vectorize. In this lesson, you'll learn how vectorize lets you apply any Python function element-wise across an array with the clean syntax of a vectorized operation — and when it's the right tool versus a true NumPy ufunc or a list comprehension.

The problem this lesson solves

Imagine you have a numeric array and you need to transform each value with a custom rule: clamp values to a range, format a number as a string, or apply a domain-specific formula. A typical approach looks like this:

import numpy as np

data = np.array([2.5, 7.8, 12.3, 19.1])

def clamp(x):
    return 5.0 if x < 5.0 else x

result = []
for x in data:
    result.append(clamp(x))

result = np.array(result)
print(result)

This works, but it's slow because Python's interpreted loop runs once per element. It also bloats your code with boilerplate. When your dataset grows to millions of rows, you'll feel every millisecond. The core pain: you want the clarity of a custom function but the performance and elegance of vectorized NumPy operations. numpy.vectorize solves this by wrapping your function so it can be called directly on an array, returning an array of results — without a manual loop.

Core concept / mental model

Think of numpy.vectorize as an adapter. You build a normal Python function — the kind you'd use with a single value — and vectorize turns it into a function that works on entire arrays. Internally, it still loops over the elements (it's not a true vectorized ufunc), but it hides that loop and adds useful features like output type specification and broadcasting.

Key terms you'll meet:

  • Element-wise: The function is applied to each item independently, without context from neighboring items.
  • Broadcasting: NumPy's ability to operate on arrays of different shapes together; vectorize respects this.
  • otypes: An optional parameter that tells NumPy the data type of the output, which speeds things up and avoids type guessing.
  • signature: An advanced feature for functions that work on 1D sub-arrays, like sliding-window calculations.

A mental diagram: your function f(x) is like a single stamp. vectorize creates a machine that stamps f across the entire array in one go, producing a new array of the same shape.

Important caveat: vectorize is not a true vectorized operation under the hood — it's still a loop in C. It's faster than a Python for loop because of internal optimizations, but for maximum performance, you'd want a real vectorized approach using NumPy's built-in functions.

How it works step by step

To apply a custom function with vectorize, follow these steps:

  1. Write a regular Python function that accepts a single value (or multiple values if your function takes more than one argument).
  2. Call np.vectorize and pass your function as the first argument. Assign the returned callable to a new name.
  3. Optionally set otypes to specify the output data type(s). This is especially important for string outputs or mixed types.
  4. Call the new function directly on a NumPy array. The result is a NumPy array of the same shape.
  5. Use the result — it behaves like any other array, so you can pass it to pandas, plot it, or do further calculations.

Here's the sequence in code:

import numpy as np

def celsius_to_fahrenheit(c):
    return (c * 9/5) + 32

# Step 2: vectorize
vec_c_to_f = np.vectorize(celsius_to_fahrenheit)

temps_c = np.array([0, 10, 20, 30, 40])
# Step 4: apply
temps_f = vec_c_to_f(temps_c)

print(temps_f)
# Output: [ 32.  50.  68.  86. 104.]

Notice how temps_f is a NumPy array with the same shape as temps_c. No loops, no list comprehensions — just a clean call.

When your function returns strings, you must specify otypes to prevent misinterpretation:

import numpy as np

def label(value):
    return "High" if value > 100 else "Low"

vec_label = np.vectorize(label, otypes=[str])

data = np.array([45, 120, 90, 200])
labels = vec_label(data)

print(labels)
# Output: ['Low' 'High' 'Low' 'High']

Without otypes=['str'], NumPy might try to guess and fail when it encounters the string.

Hands-on walkthrough

Let's tackle a realistic scenario: you have an array of product prices and you need to apply a discount and format the result as a string with a currency symbol.

import numpy as np

prices = np.array([19.99, 49.95, 99.99, 149.50])

def apply_discount(price, discount_rate=0.2):
    discounted = price * (1 - discount_rate)
    return f"${discounted:.2f}"

formatted_discount = np.vectorize(apply_discount, otypes=[str])
results = formatted_discount(prices)

print(results)
# Output: ['$16.00' '$39.96' '$80.00' '$119.60']

Now, let's use vectorize with a function that takes two array arguments — element pairs — which is a common pattern:

import numpy as np

def safe_divide(a, b):
    if b == 0:
        return np.nan
    return a / b

vec_divide = np.vectorize(safe_divide)

numerators = np.array([10, 20, 30, 40])
denominators = np.array([2, 0, 5, 4])

result = vec_divide(numerators, denominators)
print(result)
# Output: [ 5. nan  6. 10.]

Notice how nan appears where division by zero would occur — your custom logic handled it gracefully.

You can also use vectorize to create a new pandas column from an existing one, which is a very practical data analysis task:

import pandas as pd
import numpy as np

df = pd.DataFrame({'scores': [65, 89, 42, 95, 70]})

def grade(score):
    if score >= 90:
        return 'A'
    elif score >= 80:
        return 'B'
    elif score >= 70:
        return 'C'
    else:
        return 'F'

vec_grade = np.vectorize(grade)
df['grade'] = vec_grade(df['scores'])
print(df)

Output:

   scores grade
0      65     F
1      89     B
2      42     F
3      95     A
4      70     C

Expected output: The code runs without error, and each row shows the correct letter grade based on the score.

Compare options / when to choose what

You have several ways to apply a custom function to an array. Here's a comparison:

Method Pros Cons Best use case
np.vectorize Clean syntax, handles any Python function, respects broadcasting & otypes Still loops in C; not as fast as true vectorized ops When you need a quick, readable solution and performance is acceptable
List comprehension Simple, pure Python Returns a list, not a NumPy array; slower for large data Quick one-off transformations
np.frompyfunc Low-level, supports multiple outputs More complex syntax; returns object array When you need multiple outputs from one function
True vectorized ops Fastest, uses NumPy internals Only works if you can express logic with np.where, mathematical functions, etc. Performance-critical code

When to choose what: Use np.vectorize when you have a custom Python function that's hard to express with built-in NumPy functions, and you need clean code. If performance is a bottleneck, try to rewrite your logic using np.where or other vectorized functions. For most day-to-day data analysis, vectorize is a convenient middle ground.

If you need to handle multiple outputs, np.frompyfunc can do that, but it's trickier:

import numpy as np

def multi_op(x):
    return x+1, x*2

func = np.frompyfunc(multi_op, 1, 2)
a, b = func(np.array([1,2,3]))
print(a, b)  # [2 3 4] [2 4 6]

Troubleshooting & edge cases

  • object output type: If your function returns strings and you forget otypes=[str], NumPy might return an object array. This can blow up later in pandas. Always specify otypes for non-numeric outputs.
  • Slow performance: Remember that vectorize loops internally. If your code is too slow, consider refactoring to use vectorized NumPy methods, or use numba for a real speed boost.
  • Mixed types in input: If your input array has mixed types, vectorize may behave unexpectedly. Convert to a consistent dtype first.
  • Function with side effects: vectorize is meant for pure functions — ones that don't modify external state. If your function uses a counter or prints, you may get unpredictable results.
  • Unicode strings: For string results, separate by commas in otypes list to avoid format issues (e.g., `otypes=['

Practice recap

Try this: take the scores array from the lesson and write a function that maps scores to 'Pass' or 'Fail' (>=60). Use np.vectorize with otypes=['str'] and create a pandas column. Then, measure the time of vectorize versus a list comprehension using timeit — did you notice any difference? Experiment with np.frompyfunc to see how it handles multiple outputs.

Common mistakes

  • Forgetting otypes when the function returns strings, which yields an object array and breaks later pandas operations.
  • Assuming vectorize is true vectorization and expecting NumPy-level performance — it's still a loop and can be slow on huge arrays.
  • Using vectorize with functions that rely on external state or have side effects, leading to unpredictable results.
  • Not handling nan or missing values in the custom function, causing errors or wrong calculations.
  • Expecting vectorize to work with functions that span multiple elements (like sliding windows) without understanding signature.

Variations

  1. Use numpy.frompyfunc for low-level control and multiple outputs, though it returns object arrays unless you convert.
  2. Use list comprehensions for simple one-off transformations, converting to a NumPy array with np.array() when needed.
  3. For performance-critical code, rewrite logic using np.where, np.select, or other built-in ufuncs to avoid loops entirely.

Real-world use cases

  • Applying a custom data-cleaning rule (e.g., clamping outliers) to a NumPy array before feeding it into a machine learning model.
  • Creating a new pandas column with letter grades or discrete labels from a continuous score column, using np.vectorize on the Series.
  • Formatting an array of prices or metrics into strings with units or currency symbols for reporting dashboards.

Key takeaways

  • numpy.vectorize wraps a Python function to work element-wise on arrays, removing the need for manual loops.
  • You must specify otypes for functions that return non-numeric types, especially strings.
  • vectorize respects broadcasting and can handle multiple input arrays or multiple outputs via signature.
  • It is not true vectorization — performance is better than a Python loop but worse than native NumPy operations.
  • For maximum speed, use built-in ufuncs or np.where; use vectorize for readability and flexibility.
  • You can apply vectorize to pandas Series and DataFrames directly via the .values attribute or directly on Series.

Sponsored

Sponsored