Big Data

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:

CandidateExample
Legacy validation logicPAN / tax-ID checksum checker
Proprietary scoring modelsCertified credit or risk scores
Complex string/date parsingLocale-specific formats
Calls to external librariesGeocoding, 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.

KindInputUsed inExample
Eval functionOne or more fields of a tupleFOREACH ... GENERATEUPPER(name)
Filter functionA tuple, returns booleanFILTERIsValidCity(city)
Aggregate functionA whole bag, often after GROUPFOREACH on a groupMyAverage(bag)
Load / Store functionA file or streamLOAD / STOREMyJsonLoader()

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.

1. Legacy class 2. Wrap in UDF 3. Compile to jar 4. REGISTER + DEFINE 5. Use in FOREACH 1 2 3 4 5 Steps 1-3 are Java work. Steps 4-5 are Pig Latin.
The five-step path: legacy class, UDF wrapper, jar, REGISTER plus DEFINE, then use in Pig Latin.

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:

idnamecity
1RAJIVHyderabad
2OMERKolkata

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:

LibraryOriginUse
PiggybankPig communityStrings, math, dates
Apache DataFuLinkedInStatistics, sessionization, sampling
Elephant BirdTwitterThrift and Protocol Buffer formats
Your own jarIn-house codeAny 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:

LanguageEngineREGISTER form
PythonJythonREGISTER 'funcs.py' USING jython AS myfuncs;
JavaScriptRhinoREGISTER 'funcs.js' USING javascript AS js;
RubyJRubyREGISTER 'funcs.rb' USING jruby AS rb;
GroovyGroovyREGISTER '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.

Pig relation cleaned_orders legacy jar scores job Pig relation result STORE mr-input backtick cmd LOAD mr-output Pig writes the input dir, runs the jar, reads the output dir. You declare the output schema. The jar has none.
Pig to jar to Pig: STORE the input, run the backtick command, LOAD the output with a declared 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_idscore
1012.5
1020.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.

  1. Prototype in local mode with pig -x local. Use a small sample file. Local runs skip cluster queues.
  2. Build incrementally. Add one relation at a time. DUMP or DESCRIBE it before you write the next line.
  3. Name aliases well (valid_orders, not a). Add -- comments. A teammate must read the script without you.
  4. Scale up only when the logic is right. Switch to pig -x mapreduce (or Tez/Spark). Point LOAD at the full employee.csv file.
pig -x local filter_high_earners.pig
pig -x mapreduce filter_high_earners.pig

Three 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.

PatternIdeaBest for
Generate the scriptPython writes the .pig textParameters, paths, thresholds
Run via subprocessPython launches pig and checks the exit codePig as one step of a larger pipeline
Embed with compile / bind / runSingleJython drives the script from inside PigLoops 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.

compile() parse once bind() per dept runSingle() returns stats 1x 3x 3x One compile, one bind per department, one run per bind. Check stats.isSuccessful() after each run.
Embed flow: compile parses once, bind fills $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.

COUNT=4AVG=(55000+62000+71000+48000)/4=236000/4=59000.0\begin{aligned} COUNT &= \mathbf{4}\\ AVG &= (55000 + 62000 + 71000 + 48000)/4 = 236000/4 = \mathbf{59000.0} \end{aligned}

Sales. mine has 3 rows. Salaries are 48000, 51000 and 53000.

COUNT=3AVG=(48000+51000+53000)/3=152000/3=50666.67\begin{aligned} COUNT &= \mathbf{3}\\ AVG &= (48000 + 51000 + 53000)/3 = 152000/3 = \mathbf{50666.67} \end{aligned}

Marketing. mine has 3 rows. Salaries are 45000, 39000 and 42000.

COUNT=3AVG=(45000+39000+42000)/3=126000/3=42000.0\begin{aligned} COUNT &= \mathbf{3}\\ AVG &= (45000 + 39000 + 42000)/3 = 126000/3 = \mathbf{42000.0} \end{aligned}

Testing and debugging scriptsSearch on YouTube

DESCRIBE, EXPLAIN and ILLUSTRATE check a script while you build it. PigUnit tests it repeatably without a cluster.

CheckQuestion it answersRuns 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
PigUnitDoes 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.

SymptomLikely causeFix
Invalid field projectionTypo in a field nameMatch the name exactly, case included
Every value is NULLWrong delimiter in PigStorageCheck the raw file: comma, tab or pipe
ClassCastException on a fieldNo AS schema on LOADAlways declare types with AS
Ambiguous field name after JOINBoth sides share a name like idQualify it: relationName::id
Schema does not matchUNION of different shapesAlign 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 exists

Three fixes from the slides:

hadoop fs -rm -r dept_summary_output

Or 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
idnamedeptsalaryagecity
1RajivEngineering5500029Hyderabad
2PriyaEngineering6200034Bangalore
3ArjunSales4800026Mumbai
4SnehaSales5100031Delhi
5KaranMarketing4500028Pune
6DivyaEngineering7100038Hyderabad
7ManojMarketing3900024Chennai
8AnitaSales5300029Mumbai
9RakeshEngineering4800027Bangalore
10PoojaMarketing4200025Pune

Task listSearch on YouTube

  1. LOAD the file with an explicit schema into emp.
  2. DUMP and DESCRIBE emp. Confirm 10 rows and correct types.
  3. FILTER salaries above 50000 into high_earners. Confirm 5 rows.
  4. FOREACH high_earners to keep name, dept, salary.
  5. GROUP emp by dept. Confirm 3 groups.
  6. One FOREACH with COUNT, AVG and MAX per group.
  7. STORE the result into dept_summary_output. List it with hadoop fs -ls dept_summary_output (in local mode, use ls dept_summary_output). The output directory holds part-files inside.
  8. Re-run the STORE on 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.

COUNT=4SUM=55000+62000+71000+48000=236000AVG=236000/4=59000.0MAX=71000\begin{aligned} COUNT &= \mathbf{4}\\ SUM &= 55000 + 62000 + 71000 + 48000 = 236000\\ AVG &= 236000/4 = \mathbf{59000.0}\\ MAX &= \mathbf{71000} \end{aligned}

Sales. Salaries: 48000, 51000, 53000.

COUNT=3SUM=48000+51000+53000=152000AVG=152000/3=50666.67MAX=53000\begin{aligned} COUNT &= \mathbf{3}\\ SUM &= 48000 + 51000 + 53000 = 152000\\ AVG &= 152000/3 = \mathbf{50666.67}\\ MAX &= \mathbf{53000} \end{aligned}

Marketing. Salaries: 45000, 39000, 42000.

COUNT=3SUM=45000+39000+42000=126000AVG=126000/3=42000.0MAX=45000\begin{aligned} COUNT &= \mathbf{3}\\ SUM &= 45000 + 39000 + 42000 = 126000\\ AVG &= 126000/3 = \mathbf{42000.0}\\ MAX &= \mathbf{45000} \end{aligned}

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.

COUNT=3COUNT = 3. SUM=152000SUM = 152000. AVG=152000/3=50666.67AVG = 152000/3 = 50666.67. MAX=53000MAX = 53000.

7. Compute COUNT and AVG(salary) for the Marketing group: 45000, 39000, 42000.

COUNT=3COUNT = 3. SUM=126000SUM = 126000. AVG=126000/3=42000.0AVG = 126000/3 = 42000.0. MAX=45000MAX = 45000.

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, REGISTER it, DEFINE an alias, call it in FOREACH.
  • 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 MAPREDUCE operator runs a legacy jar mid-pipeline: STORE in, backtick command, LOAD out with a declared schema.
  • Develop in pig -x local on samples, one operator at a time, with DESCRIBE (schema, no job), ILLUSTRATE (sample) and limited DUMP (real job).
  • Python patterns: generate the .pig text, run it via subprocess and check the exit code, or embed it with compile / bind / runSingle for loops.
  • Lab numbers: FILTER salary > 50000 keeps 5 of 10 rows. Department summary is Engineering 4, 59000.0, 71000, Sales 3, 50666.67, 53000, Marketing 3, 42000.0, 45000.
  • PigUnit assertOutput feeds in-memory rows into one alias and checks the output alias with no cluster.