pysparkbeginner45 minPySpark

Distributed Log Tokenizer & Stopword Counter

Tokenize distributed server log files, strip punctuation, filter stopwords, and compute term frequency counts using native PySpark DataFrame transformations.

The incident

Company
CloudVigil Security
Engineer
Aditi Rao, Security Data Engineer
Trigger
Security auditors needed an automated daily audit of command executions across 500 remote Linux bastion nodes.
Stakes
Unmonitored bash histories risked.

On a Tuesday morning, Aditi Rao received an urgent security escalation at CloudVigil Security, a cloud infrastructure security firm monitoring enterprise production clusters. The security operations center detected suspicious sudo privilege escalations across staging bastion hosts. External SOC2 compliance auditors were conducting an unannounced compliance audit, giving the data engineering team only 48 hours to furnish an exhaustive audit summary of all shell commands executed across 500 remote Linux bastion instances. Security leads needed verifiable evidence that malicious actors had not breached administrative boundaries or executed unauthorized privilege escalations.

The logging agents deployed across the fleet streamed raw Linux SSH daemon authentication messages and audit logs to object storage. This generated millions of unstructured text lines every hour, totaling over 250 gigabytes of operational text data daily. The legacy ingestion script was a single-node Python program that attempted to pull entire log batches into memory using native Python file readers and the standard regular expression split method. In fast-growing cloud environments, single-node architectures quickly fail when log volumes expand beyond physical server boundaries.

The legacy script failed catastrophically: within three minutes of execution, the Python process exceeded host virtual memory limits and crashed with an Out Of Memory error. Furthermore, raw log records were cluttered with irrelevant timestamps, machine hostnames, IP addresses, punctuation, and common English stopwords like 'the', 'is', and 'for' that obscured critical security commands and created massive data noise. Manually reviewing log lines was impossible given the multi-gigabyte scale and tight compliance deadlines.

Aditi redesigned the ingestion pipeline as a native distributed PySpark job. Rather than gathering raw log lines to a single driver instance, her architecture distributed text records across a cluster of worker executors. PySpark allowed her to treat log feeds as distributed collections, executing vectorized tokenization, punctuation cleansing, and stopword filtering across executor memory without overwhelming any individual machine. By processing data in parallel partitions, the cluster scaled compute horizontally to match incoming logging velocity.

Aditi implemented the transformation pipeline using PySpark DataFrame primitives: F.split to partition text lines into token arrays, F.explode to unroll nested arrays into vertical rows, F.regexp_replace to sanitize punctuation, and ~F.col('token').isin() to remove stopwords. The distributed pipeline counted token frequencies across the cluster in a single distributed pass, isolating critical operational commands such as 'systemctl' and 'restart' alongside administrative identities like 'ubuntu'. The resulting term frequency mart provided immediate clarity on shell execution patterns.

Aditi delivered the distributed frequency report to the Lead Security Architect and the SOC2 Compliance Officer. The analysis validated that privilege escalations were confined to authorized cluster maintenance scripts, successfully clearing the compliance audit. Her distributed PySpark pipeline was promoted to production as the primary automated log tokenization engine across all CloudVigil customer environments.

What you will learn

  • Tokenize multi-word text columns into distributed rows using F.split() and F.explode()
  • Clean punctuation and standardize casing with F.lower() and F.regexp_replace()
  • Eliminate irrelevant noise by filtering against stopword arrays with ~F.col().isin()
  • Aggregate distributed term frequencies using groupBy() and count() without driver OOM

What you need first

DataFrame Column Expressions & FunctionsDataFrame basics

PySpark F.col expressions allow lazy computation across cluster nodes without pulling records into the driver Python heap.

PythonMini example
from pyspark.sql import SparkSession
from pyspark.sql import functions as F
spark = SparkSession.builder.master('local[1]').getOrCreate()
df = spark.createDataFrame([(1, 'A')], ['id', 'v'])
print(df.select(F.upper(F.col('v'))).first()[0])
A
Watch out

Calling Python standard library functions inside PySpark select statements instead of built-in F functions.

Exploding Array ColumnsHandling complex & nested data

F.explode unrolls nested array values into distinct vertical rows, establishing a clean one-row-per-token grain.

PythonMini example
from pyspark.sql import SparkSession
from pyspark.sql import functions as F
spark = SparkSession.builder.master('local[1]').getOrCreate()
df = spark.createDataFrame([(['x', 'y'],)], ['arr'])
print(df.select(F.explode('arr')).count())
2
Watch out

Attempting to group or filter inside arrays with Python loops rather than exploding arrays to DataFrame rows.

Architecture and data flow

Single Driver Memory Bottleneck
Single Driver Memory Bottleneck. Collecting gigabytes of unstructured logs into a single Python process triggers instant OutOfMemory crashes.
Distributed Explode & Aggregation Flow
Distributed Explode & Aggregation Flow. Logs are partitioned across executors, exploded into individual tokens, cleaned, and aggregated via hash exchange.
Aggregated Token Frequency Mart
Aggregated Token Frequency Mart. The final schema outputs distinct tokens and occurrences ordered by descending frequency.

The data

Raw Linux SSH daemon and sudo command execution records from cluster nodes.

server_logs: Raw Linux SSH daemon and sudo command execution records from cluster nodes.

line
2024-03-01 10:00:00 worker-1 sshd: Accepted publickey for ubuntu
2024-03-01 10:00:05 worker-1 sshd: session opened for user ubuntu
2024-03-01 10:01:00 worker-2 sudo: ubuntu : TTY=pts/0 ; COMMAND=/bin/systemctl restart spark
2024-03-01 10:02:00 worker-1 sshd: session closed for user ubuntu

Step by step

Read each step, run its code, then check your output against the one shown.

  1. Create SparkSession and ingest raw log records into DataFrame

    We initialize the SparkSession entrypoint and ingest raw server log records into a single-column DataFrame. In enterprise data engineering, wrapping raw unstructured text records into distributed DataFrames immediately unlocks Spark Catalyst query optimizer and distributes dataset partitions across worker nodes. We display the raw log lines using df.show with truncate=False to inspect timestamps, hostnames, daemon prefixes, and shell command invocations across cluster instances before executing distributed text tokenization. Inspecting the raw text schema ensures that downstream splitting rules match actual log formatting.

    PythonStep 1
    from pyspark.sql import SparkSession
    from pyspark.sql import functions as F
    
    spark = SparkSession.builder.master("local[1]").appName("audit_logs").getOrCreate()
    raw_logs = [
        ("2024-03-01 10:00:00 worker-1 sshd: Accepted publickey for ubuntu",),
        ("2024-03-01 10:00:05 worker-1 sshd: session opened for user ubuntu",),
        ("2024-03-01 10:01:00 worker-2 sudo: ubuntu : TTY=pts/0 ; COMMAND=/bin/systemctl restart spark",),
        ("2024-03-01 10:02:00 worker-1 sshd: session closed for user ubuntu",),
    ]
    df = spark.createDataFrame(raw_logs, ["line"])
    df.show(truncate=False)
    +--------------------------------------------------------------------------------------------+
    |line                                                                                        |
    +--------------------------------------------------------------------------------------------+
    |2024-03-01 10:00:00 worker-1 sshd: Accepted publickey for ubuntu                            |
    |2024-03-01 10:00:05 worker-1 sshd: session opened for user ubuntu                           |
    |2024-03-01 10:01:00 worker-2 sudo: ubuntu : TTY=pts/0 ; COMMAND=/bin/systemctl restart spark|
    |2024-03-01 10:02:00 worker-1 sshd: session closed for user ubuntu                           |
    +--------------------------------------------------------------------------------------------+
    Common mistake

    Calling collect() to fetch raw lines to the driver heap before performing distributed transformations.

    How you will know

    You will see a formatted Spark ASCII table displaying all 4 raw log lines in full width.

  2. Demonstrate driver memory exhaustion with naive local collection

    Wrong turn first: this step shows the mistake before the fix

    Here is the naive approach developers often attempt first: calling df.collect() to pull all distributed text rows onto the driver and parsing lines using Python native split and dictionary counters. While this toy example succeeds on 4 lines, executing collect() on production datasets containing millions of log records concentrates gigabytes of raw text into a single Python driver process, triggering fatal Out Of Memory exceptions and abandoning distributed cluster capabilities. We run this naive approach to highlight why centralized driver collection destroys pipeline scalability.

    PythonStep 2
    from pyspark.sql import SparkSession
    
    spark = SparkSession.builder.master("local[1]").appName("audit_logs").getOrCreate()
    raw_logs = [
        ("2024-03-01 10:00:00 worker-1 sshd: Accepted publickey for ubuntu",),
        ("2024-03-01 10:00:05 worker-1 sshd: session opened for user ubuntu",),
        ("2024-03-01 10:01:00 worker-2 sudo: ubuntu : TTY=pts/0 ; COMMAND=/bin/systemctl restart spark",),
        ("2024-03-01 10:02:00 worker-1 sshd: session closed for user ubuntu",),
    ]
    df = spark.createDataFrame(raw_logs, ["line"])
    
    # Naive approach: pulling distributed data to driver heap
    driver_rows = df.collect()
    naive_tokens = []
    for row in driver_rows:
        naive_tokens.extend(row["line"].lower().split())
    print("Collected rows to driver:", len(driver_rows))
    print("Naive token count:", len(naive_tokens))
    Collected rows to driver: 4
    Naive token count: 37
    Common mistake

    Using df.collect() and Python loops to parse text instead of native distributed DataFrame transformations.

    How you will know

    Observe that data was gathered into Python memory on the driver rather than processed on workers.

  3. Split log lines on whitespace and explode into vertical token rows

    We perform distributed tokenization across executors using native PySpark functions rather than pulling records to the driver. We apply F.lower to convert all characters to lowercase, split lines into token arrays using regex whitespace delimiters (r'\s+'), and unroll the resulting array into vertical DataFrame rows using F.explode. Each token becomes an independent row in the distributed DataFrame, establishing a clean granular representation that scales seamlessly across thousands of cluster worker nodes. This unrolling allows token-level filtering and frequency aggregations to execute in parallel across executor partitions without memory pressure.

    PythonStep 3
    from pyspark.sql import SparkSession
    from pyspark.sql import functions as F
    
    spark = SparkSession.builder.master("local[1]").appName("audit_logs").getOrCreate()
    raw_logs = [
        ("2024-03-01 10:00:00 worker-1 sshd: Accepted publickey for ubuntu",),
        ("2024-03-01 10:00:05 worker-1 sshd: session opened for user ubuntu",),
        ("2024-03-01 10:01:00 worker-2 sudo: ubuntu : TTY=pts/0 ; COMMAND=/bin/systemctl restart spark",),
        ("2024-03-01 10:02:00 worker-1 sshd: session closed for user ubuntu",),
    ]
    df = spark.createDataFrame(raw_logs, ["line"])
    tokens_df = df.select(F.explode(F.split(F.lower(F.col("line")), r"\s+")).alias("token"))
    tokens_df.show(5)
    +----------+
    |     token|
    +----------+
    |2024-03-01|
    |  10:00:00|
    |  worker-1|
    |     sshd:|
    |  accepted|
    +----------+
    only showing top 5 rows
    Common mistake

    Forgetting to lowercase strings before splitting, which causes identical words to split into distinct tokens.

    How you will know

    Output shows vertical token rows containing tokens like 2024-03-01, worker-1, and sshd:.

  4. Sanitize punctuation characters and filter irrelevant stopwords

    We sanitize raw tokens by stripping punctuation characters and filtering out non-informational English stopwords. We define an explicit list of common English stopwords and apply F.regexp_replace with pattern r'[^a-z0-9_]' to remove colons, slashes, semicolons, and hyphens. We filter for tokens with length greater than 2 characters and exclude stopwords using ~F.col('token').isin(stop_words). The tilde operator cleanly negates the membership test in PySpark column expressions. Vectorized regular expressions eliminate noisy delimiters.

    PythonStep 4
    from pyspark.sql import SparkSession
    from pyspark.sql import functions as F
    
    spark = SparkSession.builder.master("local[1]").appName("audit_logs").getOrCreate()
    raw_logs = [
        ("2024-03-01 10:00:00 worker-1 sshd: Accepted publickey for ubuntu",),
        ("2024-03-01 10:00:05 worker-1 sshd: session opened for user ubuntu",),
        ("2024-03-01 10:01:00 worker-2 sudo: ubuntu : TTY=pts/0 ; COMMAND=/bin/systemctl restart spark",),
        ("2024-03-01 10:02:00 worker-1 sshd: session closed for user ubuntu",),
    ]
    df = spark.createDataFrame(raw_logs, ["line"])
    stop_words = ["the", "and", "is", "to", "in", "a", "of", "for", "on", "it", "from", "by"]
    cleaned = (
        df
        .select(F.explode(F.split(F.lower(F.col("line")), r"\s+")).alias("token"))
        .select(F.regexp_replace(F.col("token"), r"[^a-z0-9_]", "").alias("token"))
        .filter(F.length(F.col("token")) > 2)
        .filter(~F.col("token").isin(stop_words))
    )
    cleaned.show(5)
    +--------+
    |   token|
    +--------+
    |20240301|
    |  100000|
    | worker1|
    |    sshd|
    |accepted|
    +--------+
    only showing top 5 rows
    Common mistake

    Using standard Python 'not in' syntax instead of the bitwise negation operator '~' on column isin expressions.

    How you will know

    Output shows sanitized alphanumeric tokens without punctuation or stopwords.

  5. Group by token and compute distributed term frequency counts

    In the final step, we aggregate term frequencies across the cluster using distributed groupBy and sorting operations. We call groupBy('token').count() to compute total occurrences per distinct token, and chain orderBy with descending frequency and ascending alphabetical tie-breaking. The distributed aggregation highlights 'ubuntu' and '20240301' with count 4, alongside system service identifiers. The entire pipeline executes lazily in Spark, executing partition-level map-side combiners before shuffling intermediate counts across executors, delivering high-throughput term analysis across massive distributed datasets without collecting every row onto the driver.

    PythonStep 5
    from pyspark.sql import SparkSession
    from pyspark.sql import functions as F
    
    spark = SparkSession.builder.master("local[1]").appName("audit_logs").getOrCreate()
    raw_logs = [
        ("2024-03-01 10:00:00 worker-1 sshd: Accepted publickey for ubuntu",),
        ("2024-03-01 10:00:05 worker-1 sshd: session opened for user ubuntu",),
        ("2024-03-01 10:01:00 worker-2 sudo: ubuntu : TTY=pts/0 ; COMMAND=/bin/systemctl restart spark",),
        ("2024-03-01 10:02:00 worker-1 sshd: session closed for user ubuntu",),
    ]
    df = spark.createDataFrame(raw_logs, ["line"])
    stop_words = ["the", "and", "is", "to", "in", "a", "of", "for", "on", "it", "from", "by"]
    result = (
        df
        .select(F.explode(F.split(F.lower(F.col("line")), r"\s+")).alias("token"))
        .select(F.regexp_replace(F.col("token"), r"[^a-z0-9_]", "").alias("token"))
        .filter(F.length(F.col("token")) > 2)
        .filter(~F.col("token").isin(stop_words))
        .groupBy("token")
        .count()
        .orderBy(F.col("count").desc(), F.col("token").asc())
    )
    result.show(5, truncate=False)
    +--------+-----+
    |token   |count|
    +--------+-----+
    |20240301|4    |
    |ubuntu  |4    |
    |sshd    |3    |
    |worker1 |3    |
    |session |2    |
    +--------+-----+
    only showing top 5 rows
    Common mistake

    Omitting a deterministic secondary sort column, causing non-deterministic row ordering in multi-executor runs.

    How you will know

    Output prints a table with top tokens 20240301 and ubuntu having frequency count 4.

Build it yourself

Write your solution below, run it in your browser, and pass every check.

PySpark (Catalyst Engine)Ready to run
Loading editor…
Press Run Code to evaluate
Script executes cleanly without PySpark syntax or analysis errors
Uses F.explode to unroll tokenized words into vertical rows
Correctly identifies ubuntu as a top frequent term
Calculates correct frequency count of 4 for top terms

What the result means

Interpretation

Distributed tokenization in PySpark processes high-volume log streams without collecting every row onto the driver. By unrolling tokens with F.explode, cleansing non-alphanumeric characters, and aggregating term frequencies across worker partitions, the pipeline isolates critical system commands and user identities in seconds.

The message you would send

Bastion host audit analysis complete: we processed authentication and sudo command logs across all 500 instances. The distributed frequency counter revealed that privilege escalation commands were restricted to authorized system service restarts. Full audit metrics have been generated and approved for SOC2 compliance submission.

Put it on your CV

Resume or LinkedIn bullet

OutputCopy this line
Built a distributed log tokenization and frequency counter in PySpark avoiding collecting every row onto the driver.

README for GitHub

OutputREADME.md
# Distributed Log Tokenizer & Stopword Counter

## Problem
Processing gigabytes of unstructured bastion host logs on a single node caused OutOfMemory errors and stalled security audits.

## Solution Architecture
- Distributed Tokenization: Used PySpark F.split and F.explode to distribute text parsing across worker nodes.
- Noise Cleansing: Vectorized regex replacement and stopword filtering to isolate critical commands.
- Aggregation: Distributed groupBy count to generate security audit metrics in one distributed pass.

## Results
Cleared SOC2 audit across 500 server nodes without driver memory issues.
Stretch goal

Compute document-level inverse frequency across partitioned audit log directories.

Keep going