Pandas str.count(): Count Patterns and Validate Text

Use str.count(pat, flags=0) to count regex pattern matches within each string. Escape literal search terms, choose case rules and preserve missing input. This differs from counting non-missing values or counting rows that contain a match.

Count a pattern in each string 🔝

The pattern is a regular expression. Matching is case-sensitive by default: lowercase a does not match the uppercase A in Alex. The result has one count per input value.

import pandas as pd
import re
df = pd.DataFrame({"NAME": ["Ravi", "Raju", "Alex"]})
print(df["NAME"].str.count(pat="a"))
assert df["NAME"].str.count("a").tolist() == [1, 1, 0]

Expected output

0    1
1    1
2    0
Name: NAME, dtype: int64

Store counts in a new column 🔝

Assign the returned Series to keep the count beside its source. This does not alter the source text.

df = pd.DataFrame({"NAME": ["Ravi", "Raju", "Alex"]})
df["number"] = df["NAME"].str.count(pat="a")
print(df)
assert df["number"].tolist() == [1, 1, 0]

Expected output

   NAME  number
0  Ravi       1
1  Raju       1
2  Alex       0

Count case-insensitively with flags 🔝

Use flags=re.IGNORECASE, or re.I, to match both cases. The parameter name is flags, not flag; count() has no case=False parameter.

df = pd.DataFrame({"NAME": ["Ravi", "Raju", "Alex"]})
df["number"] = df["NAME"].str.count("r", flags=re.IGNORECASE)
print(df)
assert df["number"].tolist() == [1, 1, 0]

Expected output

   NAME  number
0  Ravi       1
1  Raju       1
2  Alex       0

Escape dollar signs and other regex characters 🔝

A dollar sign means end-of-string in regex. Use a raw escaped pattern to count literal dollar signs. For a literal term supplied at runtime, re.escape() handles regex metacharacters.

df = pd.DataFrame({"NAME": ["Ravi$a", "Raj$u$b", "Alex"]})
df["number"] = df["NAME"].str.count(pat=r"\$")
print(df)
assert df["number"].tolist() == [1, 2, 0]
text = pd.Series(["C++ and C++", "A.B", "AxB"], dtype="string")
print(text.str.count(re.escape("C++")))
print(text.str.count(re.escape("A.B")))

Expected output

      NAME  number
0   Ravi$a       1
1  Raj$u$b       2
2     Alex       0
0    2
1    0
2    0
dtype: Int64
0    0
1    1
2    0
dtype: Int64

Sum occurrences across a column 🔝

sum() adds occurrences, not the number of rows with a match. In this case each matching row has one lowercase r. See sum().

names = pd.Series(["John", "Mac", "Arnold", "Krish", "Roni"], name="Names")
counts = names.str.count("r")
print(counts)
print("Total occurrences:", counts.sum())
assert counts.tolist() == [0, 0, 1, 1, 0]
assert counts.sum() == 2

Expected output

0    0
1    0
2    1
3    1
4    0
Name: Names, dtype: int64
Total occurrences: 2

Distinguish occurrences, matching rows and present values 🔝

DataFrame.count() counts non-missing values. str.count() counts pattern matches in each string. str.contains().sum() counts matching rows. These answer different questions; see DataFrame count() and matching-row totals.

text = pd.Series(["pen pen", "pen", "pencil", None], dtype="string")
counts = text.str.count("pen")
print("Occurrences:", counts.sum())
print("Matching rows:", text.str.contains("pen", regex=False, na=False).sum())
print("Present values:", text.count())
assert counts.sum() == 4
assert text.str.contains("pen", regex=False, na=False).sum() == 3

Expected output

Occurrences: 4
Matching rows: 3
Present values: 3

Keep missing input distinct from zero matches 🔝

Missing text produces a missing count, while empty strings produce zero for a non-empty literal pattern. Nullable string conversion preserves missing input. Do not treat unknown text as a confirmed zero without a deliberate policy.

text = pd.Series(["pen", "", None, 120], dtype="object").astype("string")
counts = text.str.count("pen")
print(counts)
print("Missing input:", text.isna().sum())
assert counts.iloc[0] == 1 and counts.iloc[1] == 0
assert pd.isna(counts.iloc[2])
print("Display with missing counts filled:")
print(counts.fillna(0))

Expected output

0       1
1       0
2    <NA>
3       0
dtype: Int64
Missing input: 1
Display with missing counts filled:
0    1
1    0
2    0
3    0
dtype: Int64

Count whole words rather than substrings 🔝

Word boundaries exclude pencil and open from a search for pen. Regex boundaries follow regex word-character rules; they are not a language-aware tokenizer. Keep the pattern appropriate for the text.

text = pd.Series(["pen pen", "PEN refill", "pencil", "open", "pen-holder"], dtype="string")
print(text.str.count(r"\bpen\b", flags=re.IGNORECASE))
assert text.str.count(r"\bpen\b", flags=re.IGNORECASE).tolist() == [2, 1, 0, 0, 1]

Expected output

0    2
1    1
2    0
3    0
4    1
dtype: Int64

Understand non-overlapping matches 🔝

Ordinary matches do not overlap: aa appears twice non-overlapping in aaaa. A lookahead can count overlapping starting positions, but that is a different definition and should be explicit.

text = pd.Series(["aaaa", "aaa", "aa"], dtype="string")
print("Non-overlapping:")
print(text.str.count("aa"))
print("Overlapping starts:")
print(text.str.count(r"(?=aa)"))
assert text.str.count("aa").tolist() == [2, 1, 1]
assert text.str.count(r"(?=aa)").tolist() == [3, 2, 1]

Expected output

Non-overlapping:
0    2
1    1
2    1
dtype: Int64
Overlapping starts:
0    3
1    2
2    1
dtype: Int64

Count matches rather than characters 🔝

A character class matches one character per occurrence; a + quantifier matches a whole run. Do not use an empty pattern to count characters: it matches positions between characters, including both ends. Use str.len() for length.

text = pd.Series(["AB12-CD345", "plain", "7"], dtype="string")
print("Digit characters:")
print(text.str.count(r"[0-9]"))
print("Digit runs:")
print(text.str.count(r"[0-9]+"))
assert text.str.count(r"[0-9]").tolist() == [5, 0, 1]
assert text.str.count(r"[0-9]+").tolist() == [2, 0, 1]

Expected output

Digit characters:
0    5
1    0
2    1
dtype: Int64
Digit runs:
0    2
1    0
2    1
dtype: Int64

Practical workflow: inspect a combined product field 🔝

This synthetic schema requires exactly one literal pipe separating code and description. Count separators before splitting so an extra delimiter is not hidden in the remainder. Missing input remains a separate review reason.

raw = pd.DataFrame({"product": ["0012|Notebook", "0045 | Pen", "0070", "0081|Pencil|extra", None, "|Mug"]})
text = raw["product"].astype("string")
separator_counts = text.str.count(re.escape("|"))
structure_ok = separator_counts.eq(1).fillna(False)
accepted = raw.loc[structure_ok].copy()
parts = text.loc[structure_ok].str.split("|", n=1, expand=True, regex=False).reindex(columns=[0, 1])
accepted["code"] = parts[0].str.strip()
accepted["description"] = parts[1].str.strip()
fields_ok = (accepted["code"].str.fullmatch(r"[0-9]{4}", na=False)
             & accepted["description"].notna() & accepted["description"].ne("")).fillna(False)
clean = accepted.loc[fields_ok].copy()
print(clean)
print("Missing inputs:", int(text.isna().sum()))
print("Present inputs with wrong separator count:", int((text.notna() & ~structure_ok).sum()))
print("Well-structured rows with invalid fields:", int((~fields_ok).sum()))
assert clean["code"].tolist() == ["0012", "0045"]

Expected output

         product  code description
0  0012|Notebook  0012    Notebook
1     0045 | Pen  0045         Pen
Missing inputs: 1
Present inputs with wrong separator count: 2
Well-structured rows with invalid fields: 1

Retain rejected rows and export validated fields 🔝

Structure checks are only the first stage; empty or malformed pieces still need validation. Preserve the original row index to identify rejected records. See splitting text and the cleaning workflow.

review = raw.loc[~raw.index.isin(clean.index)].copy()
print("Review rows:")
print(review)
print("Accepted rows:", len(clean), "Review rows:", len(review))
assert len(clean) == 2 and len(review) == 4
print(clean.to_csv(index=False).rstrip())
# Optional Colab export:
# clean.to_csv("validated_product_fields.csv", index=False)

Expected output

Review rows:
             product
2               0070
3  0081|Pencil|extra
4                NaN
5               |Mug
Accepted rows: 2 Review rows: 4
product,code,description
0012|Notebook,0012,Notebook
0045 | Pen,0045,Pen

Count patterns in string Index labels 🔝

The string accessor also works on an Index. Inspect label structure separately from values inside a DataFrame.

labels = pd.Index(["part_A_B", "part_C", "plain"], name="field")
print(labels.str.count("_"))
assert labels.str.count("_").tolist() == [2, 1, 0]

Expected output

Index([2, 1, 0], dtype='int64', name='field')

Common questions 🔝

Does count() join text? No; use str.cat() for concatenation. Can I pass regex=False? str.count() has no regex=False option; escape literal text with re.escape(). Why did a dot match every character? A dot is a regex wildcard. Why did totals exclude missing text? sum() skips missing counts by default; report missing inputs separately. If every input is missing, sum(min_count=1) retains an unknown total. Does counting separators validate CSV? No; quoted delimiters require a CSV parser.

Exercise: count literal plus signs 🔝

Create values A+B+C, plain, ++ and a missing value. Count literal plus signs, report total known occurrences and the number of missing inputs. Expect counts 2, 0, 2 and missing; total four.

Exercise solution 🔝

Escape the plus sign because it is a regex quantifier. Keep the missing count visible rather than treating it as known zero.

practice = pd.Series(["A+B+C", "plain", "++", None], dtype="string")
answer = practice.str.count(re.escape("+"))
print(answer)
print("Known occurrences:", answer.sum(min_count=1))
print("Missing inputs:", practice.isna().sum())
assert answer.iloc[:3].tolist() == [2, 0, 2]
assert answer.sum() == 4 and pd.isna(answer.iloc[3])

Expected output

0       2
1       0
2       2
3    <NA>
dtype: Int64
Known occurrences: 4
Missing inputs: 1

Practice in Google Colab 🔝

Open in Google Colab View on GitHub
Run the examples in order, change patterns, flags and source text, then compare occurrence counts and rejected rows. Save your own copy to keep edits. All sample tables are included in the notebook.

Continue with the Data Cleaning and String Operations hub, matching text, string length and replacing text.

Reference: Pandas Series.str.count documentation.




Subscribe to our YouTube Channel here



plus2net.com







Python Video Tutorials
Python SQLite Video Tutorials
Python MySQL Video Tutorials
Python Tkinter Video Tutorials
✖
We use cookies to improve your browsing experience. . Learn more
HTML MySQL PHP JavaScript ASP Photoshop Articles Contact us
© 2000-2026 plus2net.com All rights reserved worldwide Privacy Policy Disclaimer