Pig UDFs and scripting
This lecture extends Pig Latin with your own functions (UDFs in Java and Python), plugs legacy MapReduce jars into a pipeline with the MAPREDUCE operator, and drives Pig from Python. After this page you can write a UDF, use REGISTER and DEFINE, trace the MAPREDUCE operator, and compute the employee department summary by hand.
Why legacy Java belongs in PigSearch on YouTube
Real teams already own tested Java logic: tax-ID checks, credit scores, locale-specific parsers. Rewriting that logic in Pig Latin risks new bugs. It also duplicates maintenance work.
A UDF runs that Java class unchanged inside a Pig Latin pipeline. Pig gives the dataflow. Java gives the exact logic. Typical UDF candidates from the slides:
| Candidate | Example |
|---|---|
| Legacy validation logic | PAN / tax-ID checksum checker |
| Proprietary scoring models | Certified credit or risk scores |
| Complex string/date parsing | Locale-specific formats |
| Calls to external libraries | Geocoding, encryption, math libraries |
UDF kindsSearch on YouTube
A UDF extends Pig Latin with logic that built-in operators lack. You write it once. Then you call it like any built-in function.
| Kind | Input | Used in | Example |
|---|---|---|---|
| Eval function | One or more fields of a tuple | FOREACH ... GENERATE | UPPER(name) |
| Filter function | A tuple, returns boolean | FILTER | IsValidCity(city) |
| Aggregate function | A whole bag, often after GROUP | FOREACH on a group | MyAverage(bag) |
| Load / Store function | A file or stream | LOAD / STORE | MyJsonLoader() |
A filter function is a special Eval function. It returns true or false. An aggregate function can use the algebraic interface. That interface lets Pig use a combiner for speed.
E-F-A-L: Eval Filters, Aggregates Load
Eval runs per tuple in FOREACH. Filter keeps or drops tuples in FILTER. Aggregate folds a whole grouped bag. Load/Store plugs a custom format into LOAD or STORE.
The integration pathSearch on YouTube
Legacy code reaches Pig Latin in five steps. The slides show this path with a tax-ID validator.
The slide example registers a legacy tax-ID checker:
REGISTER /usr/local/pig/legacy_udf.jar;
DEFINE validate com.company.legacy.TaxIdValidator();
clean = FILTER records BY validate(tax_id) == true;Writing a Java UDFSearch on YouTube
Every Java Eval UDF extends org.apache.pig.EvalFunc<T>. It overrides one method: exec(Tuple input). The input tuple holds the call arguments. The return value of exec() becomes the new field value.
package myudfs;
import java.io.IOException;
import org.apache.pig.EvalFunc;
import org.apache.pig.data.Tuple;
public class UPPER extends EvalFunc<String> {
public String exec(Tuple input) throws IOException {
if (input == null || input.size() == 0) return null;
try {
String str = (String) input.get(0);
return str.toUpperCase();
} catch (Exception e) {
throw new IOException("Caught exception processing input row", e);
}
}
}Three rules from the slides. Return null on missing input. Catch bad rows and rethrow as IOException. Never let a raw exception escape.
REGISTER myudfs.jar;
DEFINE UPPER myudfs.UPPER();
upper_names = FOREACH students GENERATE UPPER(name);REGISTER points Pig at the jar file on disk. DEFINE binds the full class name to a short alias. FOREACH ... GENERATE calls the alias once per tuple.
Worked example. The students relation holds (1, Rajiv, Hyderabad) and (2, Omer, Kolkata). The script applies UPPER(name) to each tuple.
Row 1. input is (Rajiv). exec() returns Rajiv.toUpperCase(), which is RAJIV. The output tuple is (1, RAJIV, Hyderabad).
Row 2. input is (Omer). exec() returns OMER. The output tuple is (2, OMER, Kolkata).
Result. Two tuples in, two tuples out. Only the name field changes:
| id | name | city |
|---|---|---|
| 1 | RAJIV | Hyderabad |
| 2 | OMER | Kolkata |
REGISTER and DEFINESearch on YouTube
REGISTER makes a jar visible. Without an alias, you call the full class name:
REGISTER myudfs.jar;
upper_names = FOREACH students GENERATE myudfs.UPPER(name);DEFINE gives a short name. Use it when a script calls the UDF often:
REGISTER myudfs.jar;
DEFINE UPPER myudfs.UPPER();
upper_names = FOREACH students GENERATE UPPER(name);DEFINE can also pass constructor arguments. This configures the UDF at load time. The slides name this use but give no fixed syntax. The form is DEFINE alias package.Class(arg1, arg2);. DEFINE can also give a STREAM command a short alias.
Fully qualified names get long fast. Add a DEFINE alias for every UDF that a script calls more than once. Keep REGISTER paths absolute in shared scripts.
Piggybank and UDF librariesSearch on YouTube
Piggybank is a community library of ready UDFs. It ships with Pig. It covers strings, math and dates. Use it instead of writing common logic again.
REGISTER '/path/to/piggybank.jar';
DEFINE ISOToUnix org.apache.pig.piggybank.evaluation.datetime.convert.ISOToUnix();Other libraries from the slides:
| Library | Origin | Use |
|---|---|---|
| Piggybank | Pig community | Strings, math, dates |
| Apache DataFu | Statistics, sessionization, sampling | |
| Elephant Bird | Thrift and Protocol Buffer formats | |
| Your own jar | In-house code | Any Java library, same REGISTER |
Scripting UDFsSearch on YouTube
A UDF can use a scripting language. Pig runs it through an embedded interpreter. This suits quick one-off logic. No compile step. No jar to manage.
# myfuncs.py
@outputSchema("upper:chararray")
def upper(s):
return s.upper()REGISTER 'myfuncs.py' USING jython AS myfuncs;
upper_names = FOREACH students GENERATE myfuncs.upper(name);The @outputSchema line is optional. Without it Pig types the result as bytearray. Add it here to get chararray. Supported languages from the slides:
| Language | Engine | REGISTER form |
|---|---|---|
| Python | Jython | REGISTER 'funcs.py' USING jython AS myfuncs; |
| JavaScript | Rhino | REGISTER 'funcs.js' USING javascript AS js; |
| Ruby | JRuby | REGISTER 'funcs.rb' USING jruby AS rb; |
| Groovy | Groovy | REGISTER 'funcs.groovy' USING groovy AS gv; |
Worked example. myfuncs.upper runs on the city field of (2, Omer, Kolkata).
Call. Pig passes Kolkata as s. Python returns Kolkata.upper(), which is KOLKATA.
Result. The output tuple is (2, Omer, KOLKATA). The declared schema type is chararray.
The MAPREDUCE operatorSearch on YouTube
Many teams own compiled MapReduce jobs. Rewriting them in Pig Latin is not realistic. The MAPREDUCE operator runs one such jar as a single step inside a Pig pipeline. Pig Latin runs before and after it.
Pig moves the data both ways. It STOREs a relation to the path the jar expects. It LOADs the jar output back with a schema you declare. The jar itself has no schema.
result = MAPREDUCE 'legacy-job.jar'
STORE cleaned_orders INTO 'mr-input'
LOAD 'mr-output' AS (customer_id:int, score:double)
`com.example.LegacyScoringJob mr-input mr-output`;STORE ... INTO writes the named relation to the jar input path. The backtick string is the exact launch command. LOAD ... AS reads the output directory back with your schema.
Worked example (extra). cleaned_orders holds two rows: (101, 250.0) and (102, 80.0). The legacy job scores each customer as score = amount / 100.
STORE. Pig writes both rows to mr-input. The jar reads that directory.
Run. The backtick command launches com.example.LegacyScoringJob with mr-input and mr-output. The job writes (101, 2.5) and (102, 0.8) to mr-output.
LOAD. Pig reads mr-output with the declared schema. The result relation is:
| customer_id | score |
|---|---|
| 101 | 2.5 |
| 102 | 0.8 |
Do not confuse the MAPREDUCE operator with a scripting UDF. A scripting UDF runs one function per tuple. The MAPREDUCE operator runs a whole compiled job on a whole directory.
Developing Pig Latin scriptsSearch on YouTube
The slides give a four-step workflow. Follow it in order.
- Prototype in local mode with
pig -x local. Use a small sample file. Local runs skip cluster queues. - Build incrementally. Add one relation at a time.
DUMPorDESCRIBEit before you write the next line. - Name aliases well (
valid_orders, nota). Add--comments. A teammate must read the script without you. - Scale up only when the logic is right. Switch to
pig -x mapreduce(or Tez/Spark). PointLOADat the full employee.csv file.
pig -x local filter_high_earners.pig
pig -x mapreduce filter_high_earners.pigThree Python patternsSearch on YouTube
Plain Pig Latin has no loops, no conditionals and no real variables. It has only $-parameter substitution. Python adds all three. The slides give three patterns.
| Pattern | Idea | Best for |
|---|---|---|
| Generate the script | Python writes the .pig text | Parameters, paths, thresholds |
| Run via subprocess | Python launches pig and checks the exit code | Pig as one step of a larger pipeline |
Embed with compile / bind / runSingle | Jython drives the script from inside Pig | Loops and iteration, e.g. PageRank |
Only the third pattern puts Python logic around each run. The first two treat Pig as a black box. Rule of thumb from the slides: embed only when you need a loop or a condition. The first two patterns suit all else.
Generating Pig Latin from PythonSearch on YouTube
Python builds the .pig file text from a template. An f-string fills in the threshold and paths. The output is plain Pig Latin.
# generate_pig.py
SALARY_THRESHOLD = 50000
INPUT_FILE = 'employee.csv'
OUTPUT_DIR = 'high_earners_output'
script = f"""
emp = LOAD '{INPUT_FILE}' USING PigStorage(',')
AS (id:int, name:chararray, dept:chararray,
salary:int, age:int, city:chararray);
high_earners = FILTER emp BY salary > {SALARY_THRESHOLD};
STORE high_earners INTO '{OUTPUT_DIR}';
"""
with open('filter_high_earners.pig', 'w') as f:
f.write(script)
print('Generated filter_high_earners.pig')Run it with python generate_pig.py. Then open filter_high_earners.pig and check the text. The generated script is ordinary Pig Latin. Something else must launch it.
Worked example. The template above uses SALARY_THRESHOLD = 50000. The generated file holds FILTER emp BY salary > 50000.
Rows kept. From the 10-row employee.csv (see the lab below), salaries above 50000 are 55000, 62000, 51000, 71000 and 53000. That is 5 rows: Rajiv, Priya, Sneha, Divya and Anita.
Rows dropped. Salaries of 48000, 45000, 39000, 48000 and 42000 fail the test. Boundary check: no salary equals exactly 50000, so strict > and >= agree here.
Running Pig as a subprocessSearch on YouTube
subprocess.run() launches the pig command like a terminal would. Capture the output. Check the return code. React to failure in code.
# run_pig.py
import subprocess
result = subprocess.run(
['pig', '-x', 'local', 'filter_high_earners.pig'],
capture_output=True, text=True
)
if result.returncode == 0:
print('Pig job succeeded')
print(result.stdout)
else:
print('Pig job FAILED')
print(result.stderr)
raise SystemExit(1)Try it now: save the file, run python run_pig.py, then break the .pig file on purpose and re-run. The second run must print Pig job FAILED with a nonzero exit code. You get pass/fail plus logs. You get no mid-run control.
Embedding Pig Latin in PythonSearch on YouTube
Embedding differs from a scripting UDF. A scripting UDF writes one function. Embedding drives the whole script from Python. Python runs under Jython inside Pig's own JVM. Run the file with pig, not python.
$DEPT and $OUT per loop pass, runSingle runs and returns stats.from org.apache.pig.scripting import Pig
P = Pig.compile("emp = LOAD '$input' USING PigStorage(',') AS (...); ...")
bound = P.bind({'input': 'students.txt', 'city': 'Delhi', 'output': 'out'})
stats = bound.runSingle()compile() parses Pig Latin text with $params into a runnable object. bind() fills the parameters (it can bind several sets). runSingle() runs the bound script and returns a PigStats object.
The slide example loops over departments. compile() runs once. bind() runs once per department with fresh $DEPT and $OUT. Each run writes one output folder.
from org.apache.pig.scripting import Pig
pig_latin = """
emp = LOAD 'employee.csv' USING PigStorage(',')
AS (id:int, name:chararray, dept:chararray,
salary:int, age:int, city:chararray);
mine = FILTER emp BY dept == '$DEPT';
summary = FOREACH (GROUP mine ALL) GENERATE COUNT(mine), AVG(mine.salary);
STORE summary INTO '$OUT';
"""
P = Pig.compile(pig_latin)
for dept in ['Engineering', 'Sales', 'Marketing']:
out_dir = dept.lower() + '_avg_output'
bound = P.bind({'DEPT': dept, 'OUT': out_dir})
stats = bound.runSingle()
status = 'OK' if stats.isSuccessful() else 'FAILED'
print(dept, '->', status)Run it with pig embed_dept_avg.py. Confirm three output folders appear.
Worked example. The loop binds $DEPT three times over employee.csv. Each pass filters, groups all, and aggregates.
Engineering. mine has 4 rows. Salaries are 55000, 62000, 71000 and 48000.
Sales. mine has 3 rows. Salaries are 48000, 51000 and 53000.
Marketing. mine has 3 rows. Salaries are 45000, 39000 and 42000.
Testing and debugging scriptsSearch on YouTube
DESCRIBE, EXPLAIN and ILLUSTRATE check a script while you build it. PigUnit tests it repeatably without a cluster.
| Check | Question it answers | Runs a job? |
|---|---|---|
DESCRIBE rel; | Is the schema what I expect? | No |
EXPLAIN rel; | Is the execution plan right? | No |
ILLUSTRATE rel; | What happens to a small sample? | Small sample only |
DUMP rel; | Are the actual values correct? | Yes |
| PigUnit | Does the script still pass in CI? | No cluster needed |
Manual checks suit interactive work in Grunt. Nobody re-runs them before each deploy. PigUnit runs in a normal build and catches a broken FILTER or JOIN key early.
DUMP, DESCRIBE and ILLUSTRATESearch on YouTube
DUMP prints a relation to the console. Every DUMP starts a real mini MapReduce job, even in local mode. Use it on purpose, not after each keystroke. Never DUMP a huge relation. Limit it first.
-- cheap: schema only, no job runs
DESCRIBE emp;
-- safe peek at big data: only 5 rows
DUMP (LIMIT emp 5);
-- done: write the full result
STORE dept_summary INTO 'dept_summary_output' USING PigStorage(',');Schema and field errorsSearch on YouTube
When Pig complains, run DESCRIBE on every relation above the failing line first. Fix the schema before you touch logic.
| Symptom | Likely cause | Fix |
|---|---|---|
| Invalid field projection | Typo in a field name | Match the name exactly, case included |
| Every value is NULL | Wrong delimiter in PigStorage | Check the raw file: comma, tab or pipe |
| ClassCastException on a field | No AS schema on LOAD | Always declare types with AS |
Ambiguous field name after JOIN | Both sides share a name like id | Qualify it: relationName::id |
| Schema does not match | UNION of different shapes | Align field counts and types first |
STORE and existing directoriesSearch on YouTube
STORE writes final output to the file system. It does not print to the console. Pig refuses to overwrite an existing output directory. This guards results against silent loss. On re-run you see:
Output Location Validation Failed: 'dept_summary_output' already existsThree fixes from the slides:
hadoop fs -rm -r dept_summary_outputOr use a fresh path each run (dept_summary_output_v2). Or wrap both steps in a shell script: delete with hadoop fs -rm -r -f out, then run pig myscript.pig.
PigUnit testsSearch on YouTube
PigUnit is JUnit-based. It swaps real LOAD/STORE for in-memory rows. It runs the script and asserts on the named output relation.
PigTest test = new PigTest("script.pig");
String[] input = {
"1\tRajiv\tHyderabad",
"2\tOmer\tKolkata"
};
String[] output = {
"(1,RAJIV,Hyderabad)",
"(2,OMER,Kolkata)"
};
test.assertOutput("students", input, "upper_names", output);assertOutput() feeds input into the students alias. It runs the script through upper_names. It fails the test on any mismatch. No file system or cluster takes part.
Worked example. The test above feeds 2 tab-separated rows into students. The script loads with PigStorage('\t') semantics and applies UPPER(name).
Row 1. (1, Rajiv, Hyderabad) becomes (1, RAJIV, Hyderabad). This matches expected row 1.
Row 2. (2, Omer, Kolkata) becomes (2, OMER, Kolkata). This matches expected row 2. The test passes 2 of 2.
Employee analytics labSearch on YouTube
This lab joins all ideas of the lecture: load, filter, project, group, aggregate, store, then automate with Python.
The employee dataSearch on YouTube
employee.csv has 10 rows with schema (id:int, name:chararray, dept:chararray, salary:int, age:int, city:chararray). Push it to HDFS first on a real cluster:
hadoop fs -mkdir -p /pig_lab
hadoop fs -put employee.csv /pig_lab/employee.csv| id | name | dept | salary | age | city |
|---|---|---|---|---|---|
| 1 | Rajiv | Engineering | 55000 | 29 | Hyderabad |
| 2 | Priya | Engineering | 62000 | 34 | Bangalore |
| 3 | Arjun | Sales | 48000 | 26 | Mumbai |
| 4 | Sneha | Sales | 51000 | 31 | Delhi |
| 5 | Karan | Marketing | 45000 | 28 | Pune |
| 6 | Divya | Engineering | 71000 | 38 | Hyderabad |
| 7 | Manoj | Marketing | 39000 | 24 | Chennai |
| 8 | Anita | Sales | 53000 | 29 | Mumbai |
| 9 | Rakesh | Engineering | 48000 | 27 | Bangalore |
| 10 | Pooja | Marketing | 42000 | 25 | Pune |
Task listSearch on YouTube
LOADthe file with an explicit schema intoemp.DUMPandDESCRIBEemp. Confirm 10 rows and correct types.FILTERsalaries above 50000 intohigh_earners. Confirm 5 rows.FOREACHhigh_earnersto keepname,dept,salary.GROUPempbydept. Confirm 3 groups.- One
FOREACHwithCOUNT,AVGandMAXper group. STOREthe result intodept_summary_output. List it withhadoop fs -ls dept_summary_output(in local mode, usels dept_summary_output). The output directory holds part-files inside.- Re-run the
STOREon purpose. Reproduce the exists error. Fix it.
Reference solutionSearch on YouTube
emp = LOAD 'employee.csv' USING PigStorage(',')
AS (id:int, name:chararray, dept:chararray,
salary:int, age:int, city:chararray);
high_earners = FILTER emp BY salary > 50000;
emp_slim = FOREACH high_earners GENERATE name, dept, salary;
grouped = GROUP emp BY dept;
dept_summary = FOREACH grouped GENERATE
group AS department,
COUNT(emp) AS emp_count,
AVG(emp.salary) AS avg_salary,
MAX(emp.salary) AS max_salary;
STORE dept_summary INTO 'dept_summary_output' USING PigStorage(',');Worked example. GROUP emp BY dept makes 3 groups. Group sizes are 4, 3 and 3. They sum to 10. No row is lost.
Engineering. Salaries: 55000, 62000, 71000, 48000.
Sales. Salaries: 48000, 51000, 53000.
Marketing. Salaries: 45000, 39000, 42000.
Output. dept_summary_output holds three rows: Engineering,4,59000.0,71000, Sales,3,50666.67,53000, Marketing,3,42000.0,45000. Sanity check: each average sits between its group minimum and maximum.
Bonus: configurable generator (extra)Search on YouTube
Hard-code nothing. Take the department and threshold from the command line. Write one .pig file per department so reruns do not clash.
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('--dept', required=True)
parser.add_argument('--min-salary', type=int, default=50000)
args = parser.parse_args()
out_file = f'{args.dept.lower()}_filtered.pig'
script = f"""
emp = LOAD 'employee.csv' USING PigStorage(',')
AS (id:int, name:chararray, dept:chararray,
salary:int, age:int, city:chararray);
result = FILTER emp BY dept == '{args.dept}' AND salary > {args.min_salary};
DUMP result;
"""
with open(out_file, 'w') as f: f.write(script)Run python generate_pig.py --dept Engineering --min-salary 60000. The file engineering_filtered.pig keeps Engineering rows above 60000: Priya (62000) and Divya (71000). Rajiv (55000) and Rakesh (48000) drop out.
Quick self-testSearch on YouTube
1. Name the four UDF kinds and state where each one is used.
Eval in FOREACH ... GENERATE. Filter in FILTER (returns boolean). Aggregate on a grouped bag in FOREACH. Load/Store in LOAD or STORE for custom formats.
2. Which command starts a real MapReduce job: DESCRIBE or DUMP?
DUMP. DESCRIBE only shows the schema and starts no job.
3. True or false: STORE silently overwrites an existing output directory.
False. Pig refuses with Output Location Validation Failed. Delete the directory or use a fresh path.
4. Which statement makes a compiled UDF jar visible to a script?
REGISTER. Then DEFINE gives the class a short alias for use in FOREACH.
5. FILTER emp BY salary greater than 50000 keeps how many rows of the 10-row lab set?
5 rows: Rajiv (55000), Priya (62000), Sneha (51000), Divya (71000) and Anita (53000).
6. Compute COUNT and AVG(salary) for the Sales group: 48000, 51000, 53000.
. . . .
7. Compute COUNT and AVG(salary) for the Marketing group: 45000, 39000, 42000.
. . . .
8. What error does a misspelled field name in FOREACH ... GENERATE raise?
Invalid field projection at compile time. Fix it with DESCRIBE: match the name exactly, case included.
9. Which Python module launches a Pig script as a subprocess, and what must you check?
subprocess (e.g. subprocess.run). Check returncode: 0 means success. Print stderr and exit nonzero on failure.
10. Differentiate the three Python patterns. When must you pick embedding?
Generate writes .pig text for later runs. Subprocess launches pig and sees only pass/fail. Embed (compile / bind / runSingle) adds loops and conditions. Pick embedding for iteration such as one run per department. It needs Jython and runs via pig, not python.
- Legacy Java enters Pig as a UDF: wrap the class in
EvalFunc, compile to a jar,REGISTERit,DEFINEan alias, call it inFOREACH. - UDF kinds: Eval (per tuple), Filter (boolean, in
FILTER), Aggregate (whole grouped bag, algebraic for speed), Load/Store (custom formats). - Piggybank, DataFu and Elephant Bird supply ready UDFs. Scripting UDFs use Jython, Rhino, JRuby or Groovy with no jar.
- The
MAPREDUCEoperator runs a legacy jar mid-pipeline:STOREin, backtick command,LOADout with a declared schema. - Develop in
pig -x localon samples, one operator at a time, withDESCRIBE(schema, no job),ILLUSTRATE(sample) and limitedDUMP(real job). - Python patterns: generate the
.pigtext, run it viasubprocessand check the exit code, or embed it withcompile/bind/runSinglefor loops. - Lab numbers:
FILTER salary > 50000keeps 5 of 10 rows. Department summary is Engineering4, 59000.0, 71000, Sales3, 50666.67, 53000, Marketing3, 42000.0, 45000. - PigUnit
assertOutputfeeds in-memory rows into one alias and checks the output alias with no cluster.
Apache Pig Tutorial 1
User Defined Functions in PIG Using JAVA
Pig UDFs
Demo on Pig UDF