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
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;
vectorizerespects 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:
- Write a regular Python function that accepts a single value (or multiple values if your function takes more than one argument).
- Call
np.vectorizeand pass your function as the first argument. Assign the returned callable to a new name. - Optionally set
otypesto specify the output data type(s). This is especially important for string outputs or mixed types. - Call the new function directly on a NumPy array. The result is a NumPy array of the same shape.
- 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
objectoutput type: If your function returns strings and you forgetotypes=[str], NumPy might return an object array. This can blow up later in pandas. Always specifyotypesfor non-numeric outputs.- Slow performance: Remember that
vectorizeloops internally. If your code is too slow, consider refactoring to use vectorized NumPy methods, or usenumbafor a real speed boost. - Mixed types in input: If your input array has mixed types,
vectorizemay behave unexpectedly. Convert to a consistent dtype first. - Function with side effects:
vectorizeis 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
otypeslist 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
otypeswhen the function returns strings, which yields an object array and breaks later pandas operations. - Assuming
vectorizeis true vectorization and expecting NumPy-level performance — it's still a loop and can be slow on huge arrays. - Using
vectorizewith functions that rely on external state or have side effects, leading to unpredictable results. - Not handling
nanor missing values in the custom function, causing errors or wrong calculations. - Expecting
vectorizeto work with functions that span multiple elements (like sliding windows) without understandingsignature.
Variations
- Use
numpy.frompyfuncfor low-level control and multiple outputs, though it returns object arrays unless you convert. - Use list comprehensions for simple one-off transformations, converting to a NumPy array with
np.array()when needed. - 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.vectorizeon the Series. - Formatting an array of prices or metrics into strings with units or currency symbols for reporting dashboards.
Key takeaways
numpy.vectorizewraps a Python function to work element-wise on arrays, removing the need for manual loops.- You must specify
otypesfor functions that return non-numeric types, especially strings. vectorizerespects broadcasting and can handle multiple input arrays or multiple outputs viasignature.- 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; usevectorizefor readability and flexibility. - You can apply
vectorizeto pandas Series and DataFrames directly via the.valuesattribute or directly on Series.