← All resources

Concatenate Columns Pandas

14 min read
Concatenate Columns Pandas

You've got first_name and last_name in an order export, but the CRM needs full_name. To concatenate columns in pandas, choose among the + operator, Series.str.cat(), pd.concat(..., axis=1), and DataFrame.assign(). The right choice depends less on syntax than on index alignment, missing values, and dtype control.

A reliable workflow treats column concatenation as a typing and indexing problem first. That distinction matters whether you're building customer identifiers, reporting labels, model features, or cleaned ETL outputs.

Table of Contents

Why Concatenating Columns Comes Up Everywhere

The most familiar case is a name column. An order export arrives with first_name and last_name separate, while the downstream CRM expects one full_name field. For clean string columns, the operation looks simple:

df["full_name"] = df["first_name"] + " " + df["last_name"]

The same pattern appears in less obvious places. You might construct a customer_id from account_number and region_code before joining to another table, or combine year, quarter, and product_code into a reporting label such as 2026-Q1-ABC.

Feature engineering uses the same idea. A model may benefit from a combined state_city location flag, while an ETL cleanup step may merge vendor and SKU fields into a stable product_label. These are all column-concatenation tasks, but they don't all have the same data risks.

Practical rule: Before writing the expression, inspect the source dtypes, missing-value representation, and index. Most production failures come from one of those three inputs.

Pandas documents concat() as a core operation for combining objects along an axis. Its axis=1 mode returns a DataFrame and supports union or intersection logic on the non-concatenation axis, making horizontal assembly part of pandas' foundational data-wrangling model rather than a niche extension. The pandas concatenation documentation has described this framework since at least May 25, 2016.

There are four practical techniques:

  • The plus operator, for quick merges of two string Series.
  • str.cat(), when separators and missing-value policy need to be explicit.
  • pd.concat(..., axis=1), when you're assembling complete columns from separate objects.
  • DataFrame.assign(), when derived columns should remain inside a method chain.

For a broader cleaning workflow, it helps to place concatenation alongside validation, standardization, and missing-data decisions. This practical guide to cleaning data provides the wider context.

The Four Ways to Concatenate Columns in Pandas

Each method wins at a different job. The mistake is choosing based only on which expression is shortest.

Method Best for Handles non-string NaN behavior Scales to many columns
+ Two readable string merges Cast explicitly first Missing values can contaminate output Poorer readability as operands grow
str.cat() Delimiters and missing-value policy Cast offending columns first Controlled with na_rep Good for string-oriented assembly
pd.concat(..., axis=1) Side-by-side Series or DataFrames Yes, preserves column types where possible Index alignment can create gaps Good for assembling many objects
assign() Derived columns in a pipeline Yes, if expressions handle types Follows the calling frame's index Good for chained transformations

For first_name and last_name, use + when both columns are already reliable strings and the expression is unlikely to grow. If a delimiter or explicit missing-value replacement matters, str.cat() is clearer.

Use pd.concat() when the inputs already exist as separate Series or DataFrames and may have different dtypes. It's an assembly operation, not primarily a string-formatting operation.

assign() is the cleanest option inside a transformation pipeline:

clean = (
    df
    .assign(
        full_name=lambda x: x["first_name"].str.cat(
            x["last_name"], sep=" "
        )
    )
)

That expression keeps the derived column attached to the frame while allowing additional transformations to follow.

The key distinction is scope. + and str.cat() create values from string-like operands. concat() combines labeled pandas objects. assign() derives new columns against an existing frame.

String Columns with str.cat and the Plus Operator

For two known string columns, the plus operator is direct:

df["full_name"] = df["first_name"] + " " + df["last_name"]

It reads well and works nicely when both Series contain genuine strings. Problems start when one field contains integers, mixed Python objects, or missing values. An expression such as this can raise a TypeError if last_name is numeric or otherwise non-string:

df["label"] = df["prefix"] + "-" + df["code"]

str.cat() makes the string operation more explicit:

df["full_name"] = df["first_name"].str.cat(
    df["last_name"],
    sep=" "
)

The receiver is the left-hand Series, here df["first_name"]. The second Series is passed into str.cat(). That matters when you're reading or debugging a longer chain.

Screenshot from https://pandas.pydata.org/docs/reference/api/pandas.Series.str.cat.html

A non-string operand still needs conversion. str.cat() isn't a universal coercion layer, so cast the offending side deliberately:

df["customer_id"] = (
    df["account_number"].astype(str)
    .str.cat(df["region_code"].astype(str), sep="-")
)

The missing-value policy is another reason to prefer str.cat(). With the plus operator, a missing value can propagate into the result rather than being omitted cleanly. With str.cat(), you can specify a replacement:

df["display_name"] = df["first_name"].str.cat(
    df["last_name"],
    sep=" ",
    na_rep="_"
)

That decision should reflect the downstream meaning. An underscore may be useful for a machine-readable identifier, but an empty string or a preserved missing value may be more appropriate for a report.

The pandas API reference documents str.cat() as the string-specific operation, including separator and missing-value controls. If you're working with a slice, assign safely to a copy first:

subset = df.loc[df["region_code"].notna()].copy()
subset["full_name"] = subset["first_name"].str.cat(
    subset["last_name"],
    sep=" "
)

Writing directly through an ambiguous slice can trigger SettingWithCopyWarning, which signals that pandas can't guarantee whether you're modifying the original frame or a detached view.

The following walkthrough is useful when you want to see the operation in context:

Building Full Columns with concat and assign

pd.concat(..., axis=1) is the right tool when you have complete Series or DataFrames and want to place them side by side:

result = pd.concat(
    [df["first_name"], df["order_total"]],
    axis=1
)

The important behavior is label-based alignment. Pandas aligns rows by index, not by physical row position. If the labels don't match, the result contains NaN gaps under non-overlapping labels. The official pandas.concat() reference documents this axis behavior and the resulting DataFrame.

Consider two objects with different index schemes:

names = pd.Series(
    ["Alice", "Bob"],
    index=pd.Index(["u1", "u2"], name="user_id"),
    name="name"
)

orders = pd.Series(
    [10, 20],
    name="orders"
)

result = pd.concat([names, orders], axis=1)

names is indexed by u1 and u2, while orders uses a default integer index. Pandas aligns labels rather than assuming that the first row belongs with the first row. The resulting columns won't pair as intended, and missing entries appear where labels don't overlap.

Fix the mismatch intentionally. If row position is the key, normalize both indexes.

result = pd.concat(
    [
        names.reset_index(drop=True),
        orders.reset_index(drop=True)
    ],
    axis=1
)

If labels carry meaning, reindex instead:

orders = orders.reindex(names.index)
result = pd.concat([names, orders], axis=1)

assign() follows a different pattern. It derives columns against the calling DataFrame's index:

result = (
    df
    .assign(
        customer_id=lambda x:
            x["account_number"].astype(str)
            + "-"
            + x["region_code"].astype(str)
    )
)

assign() returns a new DataFrame and chains naturally. It's usually preferable for in-place-style derivation, while concat() is better when source columns live in separate frames.

Aspect concat(..., axis=1) DataFrame.assign()
Primary job Assemble labeled objects horizontally Derive columns on an existing frame
Alignment Aligns source objects by index Uses the calling frame's index
Mixed source frames Strong fit Less direct
Method chaining Possible, but less expressive Natural
Series labels A named Series supplies its column label Keyword or expression supplies the label

Name standalone Series before concatenating them:

score = pd.Series([4, 5], name="score")

Without a meaningful name, pandas may fall back to a generic or unclear column label. For transformation design beyond this narrow operation, these data transformation techniques offer useful patterns.

What Happens to NaN and Dtypes Under the Hood

A common assumption is that string concatenation skips missing values. The plus operator doesn't reliably do that. Given a string value and a missing value, you can end up with a literal-looking contamination such as JohnNaN, depending on the underlying representation and operation.

left = pd.Series(["John"])
right = pd.Series([float("nan")])

plus_result = left.astype(str) + right.astype(str)
cat_result = left.str.cat(right, na_rep="_")

print(plus_result.iloc[0])
# Johnnan

print(cat_result.iloc[0])
# John_

The exact capitalization of the converted missing token depends on the conversion path, which is precisely why implicit coercion is risky. The important point is that the plus operator doesn't know your business rule for missing data. str.cat() lets you state that rule explicitly.

A comparison image showing that adding a NaN value to a string results in JohnNaN in code.

Dtype changes create a second class of bugs. Concatenating a float64 column with an int64 column can promote the result to float64, particularly when missing values require a floating representation. Combining a string column with a numeric column often produces object, which can prevent the result from behaving like a clean string Series in later vectorized operations.

A safer mixed-type pattern is explicit conversion:

df["new"] = (
    df["a"].astype(str)
    + "-"
    + df["b"].astype(str)
)

That expression is readable and makes the coercion visible in code review. It also avoids relying on pandas to infer how an identifier should be represented.

Type check: After concatenation, inspect result.dtypes, check the output for unexpected missing tokens, and verify that the index still represents the intended row identity.

Categorical columns deserve separate caution. Once a categorical field participates in a string concatenation, the result generally becomes a plain object-like string result rather than retaining the original category list. That can remove the memory and validation advantages you expected from categorical storage.

For missing-data decisions, don't treat a formatting operation as an imputation strategy. This guide to handling missing data is the better place to decide whether a missing value should be preserved, replaced, or excluded.

Performance Pitfalls and the Vectorized Shortcut

pd.concat() is convenient, but repeated use can make a column-building refactor unexpectedly slow. Each call may copy existing blocks and consolidate internal structures, so the cost grows with the object being rebuilt. The pandas performance guidance for concat() recommends collecting objects first and concatenating once.

Avoid this pattern:

wide = pd.DataFrame(index=df.index)

for column in columns:
    wide = pd.concat([wide, df[[column]]], axis=1)

Every iteration creates a wider DataFrame. Build the list first:

parts = [df[[column]] for column in columns]
wide = pd.concat(parts, axis=1)

The same rule applies when chunks arrive incrementally. Keep each chunk in a list, then call pd.concat() once after collection. This reduces repeated copying and makes the intended index behavior easier to inspect.

For string columns, a vectorized expression is usually the better default than a Python loop:

df["full_name"] = (
    df["first"].astype(str)
    + " "
    + df["last"].astype(str)
)

One independent benchmark found a large gap between string-construction approaches. The direct vectorized-style method was roughly an order of magnitude faster than the slowest join-based approach in that test. Treat the direction, not the exact timing, as the useful takeaway. Results still depend on frame size, dtype, missing values, and the operation being measured.

Loop test: If a loop combines values from the same row into a new column, check whether +, str.cat(), assign(), or one batch concat() expresses the operation.

Wide frames can also exhaust memory through temporary copies. Add derived columns in coherent groups, validate dtypes as each group is created, and release intermediate objects when they are no longer needed. Pair performance checks with data profiling practices, so unexpected object columns or index changes are caught before they become production costs.

Choosing the Right Method and Skipping It Altogether

Keep the decision rule short:

  • Use str.cat() when the operands are string-oriented and you need a separator or explicit NaN policy.
  • Use + with .astype(str) when readability matters and source types are mixed.
  • Use concat(..., axis=1) when you're assembling pre-built Series or DataFrames and index alignment is intentional.
  • Use assign() when several derived columns should remain inside a fluent transformation chain.

If you control ingestion, establish appropriate string dtypes at read time where practical. That reduces later coercion surprises, but it doesn't remove the need to inspect missing values or verify the resulting dtype.

On very wide frames, build and validate in groups rather than creating a large number of intermediate copies. For identifiers, preserve leading zeros deliberately. Converting a code to a numeric type before string assembly can destroy information that isn't recoverable later.

A diagram comparing three methods for concatenating columns in pandas: str.cat, plus operator with astype, and concat.

Frequently Asked Questions

How do I concatenate two columns in pandas?

Use the plus operator for clean string columns:

df["full_name"] = df["first_name"] + " " + df["last_name"]

For mixed types, cast explicitly:

df["label"] = (
    df["year"].astype(str)
    + "-"
    + df["quarter"].astype(str)
)

How do I concatenate columns while handling NaN?

Use Series.str.cat() with na_rep when you need a defined replacement:

df["label"] = df["left"].str.cat(
    df["right"],
    sep="_",
    na_rep=""
)

Choose the replacement based on the meaning of the field. An empty string, placeholder, and preserved missing value are not interchangeable.

How do I concatenate columns from different DataFrames?

Use pd.concat() with axis=1:

result = pd.concat([left, right], axis=1)

Check the indexes first. Pandas aligns by index labels, so different indexes can produce NaN gaps rather than a position-by-position merge.

What is the difference between concat and merge?

concat() assembles objects along an axis. merge() matches rows using key columns or indexes. If you're building a label from fields in the same row, neither operation is necessary. If you're combining independently keyed datasets, merge() may express the intent more accurately.

How do I concatenate many columns efficiently?

Use a vectorized string expression for a known set of fields, or collect objects in a list and call pd.concat() once. Avoid repeatedly growing a DataFrame inside a loop because each operation may copy and consolidate existing data.

For analysts who want to move beyond manually selecting the right pandas expression, agentic analytics changes the workflow. An AI data analyst can inspect the columns, infer likely join keys, identify dtype conflicts, run the transformation, check the result, and preserve the code rather than returning a one-off answer. That distinction matters because an answer is a data point, while an analysis is actionable, reproducible intelligence.

PlotStudio applies that model locally. You upload a dataset, review the proposed plan in Plan Mode, and let the embedded Python engine write and execute real code, inspect its output, self-correct, and save the result as an Analysis Page with narrative, charts, statistics, and code. The workflow is designed for individual analysts and researchers rather than enterprise dashboard monitoring, with local execution, inspectable transformations, and export to Jupyter or PDF.

The practical difference from chat-with-your-data tools is persistence. Instead of losing a column-cleaning decision in a conversation, you can retain the analysis, refer back to it, and build later investigations from the saved work. For sensitive research data, local execution also keeps the dataset on your machine.


If you're tired of hand-checking dtype coercion and index alignment before every transformation, try PlotStudio AI for local, agentic analysis that writes and runs the Python, validates its work, and saves a reproducible Analysis Page. Upload a dataset, review the plan, and use the generated code as the auditable starting point for your next analysis.