How can I count the occurrence of all the names in the names list which have the letter 'i' as their second letter

Viewed 88

I am trying to count the occurrence of the name in a name_list with letter 'i' as their second letter using a nested loop.

 def print_count(names_list):
    for name in names_list:
        count = 0
        for i in range(len(name)):
            if name[i] == 'i' and i == 1:
                count = count + 1

    print(count)

names = ["Cody", "Baldassar", "Delilah", "Vinnie", "Leila", "Zac", "Aiden", "Zaynab"]
print_count(names)

My expected output should be: 2 but i got 0 instead.

6 Answers

Update

The fastest solution so far is @KellyBundy's idea of using a slice:

>>> len([s for s in names if s[1:2] == 'i'])
2

Original answer (twice slower!)

You can express that simply and efficiently:

>>> len([s for s in names if s[1:].startswith('i')])
2

Why?

The argument of len is a list comprehension. It is the original list, filtered by the condition "must have a second letter, and that second letter must be 'i'":

>>> [s for s in names if s[1:2] == 'i']
['Vinnie', 'Aiden']

But is it safe?

Q: "What if a word is empty or has only one letter? For sure s[1] would raise IndexError, right?"

A: It is safe. Yes, s[1] would raise if s is empty or contains a single char, but s[1:2] is just fine:

>>> 'foo'[1:2]
'o'

>>> 'f'[1:2]
''

>>> ''[1:2]
''

Variations and timings

# setup: generate a large list of random names
import numpy as np

n = 1_000_000
names = list(map(''.join, np.random.choice(list('abcdefghijkl'), (n, 10))))

# 1. ***current winner*** @KellyBundy's idea to use a slice
%timeit len([s for s in names if s[1:2] == 'i'])
# 71.8 ms ± 32.2 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

# 2. @Blckknght's suggestion, len of list comprehension version
%timeit len([s for s in names if len(s) > 1 and s[1] == 'i'])
# 98.2 ms ± 82.3 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

# 3. @Blckknght's suggestion, generator version
%timeit sum(len(s) > 1 and s[1] == 'i' for s in names)
# 105 ms ± 24.7 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

# 4. @AndrejKesely's solution
%timeit sum(n[1] == "i" for n in names if len(n) > 1)
# 106 ms ± 77.7 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

# 5. original answer
%timeit len([s for s in names if s[1:].startswith('i')])
# 140 ms ± 821 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

# 6. generator
%timeit sum(1 for s in names if s[1:].startswith('i'))
# 141 ms ± 27.4 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

# 7. sum of booleans, as list comprehension
%timeit sum([s[1:].startswith('i') for s in names])
# 154 ms ± 145 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

# 8. sum of booleans, as generator
%timeit sum(s[1:].startswith('i') for s in names)
#163 ms ± 544 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

Notice how operating on a generator instead of on a list comprehension sometimes takes longer (# 3. vs # 2. and # 8. vs # 7.). That surprised me when I heard about it a few years ago.

Update (including @Blckknght and @AndrejKesely ideas)

Both of these solutions are about 40% faster than my initial code (kudos!).

Update 2 Including @KellyBundy's slice idea

That idea (new # 1.) gets us another 20% cut off of the previous winner (# 2. @Blckknght's suggestion together with using len of a list comprehension). It is the new overall winner. In my tests, I found that using a constant slice (slc = slice(1, 2) and s[slc]) is indistinguishable from the expression in # 1.

Add count = 0 above the for loop. Otherwise, you reset it to zero each time. In addition, you do not need the second loop.

def print_count(names_list):
    count = 0
    for name in names_list:
        if len(name) > 1 and name[1] == 'i':
            count += 1
    print(count)

names = ["Cody", "Baldassar", "Delilah", "Vinnie", "Leila", "Zac", "Aiden", "Zaynab"]
print_count(names)

You are setting count to zero for every name because you placed count=0 inside the loop that loops over the names. The way it is now, the count that is printed only contains the count of the last name in the list.

If you move it out of the loop your code will work.

Another solution:

print(sum(n[1] == "i" for n in names if len(n) > 1))

Prints:

2

Quick benchmark:

from timeit import timeit

names = [
    "Cody",
    "Baldassar",
    "Delilah",
    "Vinnie",
    "Leila",
    "Zac",
    "Aiden",
    "Zaynab",
]

def fn1():
    return len([s for s in names if s[1:].startswith("i")])


def fn2():
    return sum(n[1] == "i" for n in names if len(n) > 1)


t1 = timeit(lambda: fn1(), number=1_000_000)
t2 = timeit(lambda: fn2(), number=1_000_000)

print(t1)
print(t2)

Prints on my machine (AMD 3700X, Pyhton 3.9.7):

1.5491739970020717
1.2039272840047488

Some more solutions/benchmarks:

 625 us ± 3.9 us  [n[i12] for n in names].count("i")
 632 us ± 3.5 us  len([0 for n in names if "i" in n[i12]])
 654 us ± 2.3 us  len([0 for n in names if n[i12] == "i"])
 657 us ± 4.0 us  len([n for n in names if n[i12] == "i"])
 691 us ± 3.9 us  sum(1 for n in names if n[i12] == "i")
 736 us ± 5.9 us  [*map(ig, names)].count("i")
 777 us ± 5.5 us  countOf(map(ig, names), "i")
 837 us ± 5.1 us  sum(1 for n in names if n[1:2] == "i")
1038 us ± 1.6 us  len([s for s in names if s.startswith("i", 1)])
1206 us ± 5.0 us  sum(n[1:2] == "i" for n in names)
1229 us ± 3.2 us  sum(n[1] == "i" for n in names if len(n) > 1)
1594 us ± 4.9 us  len([s for s in names if s[1:].startswith("i")])

Benchmark code (Try it online!):

from timeit import timeit, repeat
from random import shuffle
from statistics import mean, stdev

setup = '''
from operator import itemgetter, countOf

names = [
    "Cody",
    "Baldassar",
    "Delilah",
    "Vinnie",
    "Leila",
    "Zac",
    "Aiden",
    "Zaynab",
] * 1000

i12 = slice(1, 2)
ig = itemgetter(i12)
'''

codes = [
  'len([s for s in names if s[1:].startswith("i")])',
  'sum(n[1] == "i" for n in names if len(n) > 1)',
  'len([s for s in names if s.startswith("i", 1)])',
  'sum(n[1:2] == "i" for n in names)',
  'sum(1 for n in names if n[1:2] == "i")',
  'sum(1 for n in names if n[i12] == "i")',
  'len([0 for n in names if n[i12] == "i"])',
  'len([0 for n in names if "i" in n[i12]])',
  'len([n for n in names if n[i12] == "i"])',
  'countOf(map(ig, names), "i")',
  '[n[i12] for n in names].count("i")',
  '[*map(ig, names)].count("i")',
]

for c in codes:
  exec(setup)
  print(eval(c), c)

fs = codes
T = {f: [] for f in fs}
def s(f):
  ts = [t*1e6 for t in sorted(T[f])[:5]]
  return f'{int(mean(ts)):4} us ± {stdev(ts):.1f} us '

for _ in range(100):
  shuffle(fs)
  for f in fs:
    number = 10
    t = timeit(f, setup, number=number) / number
    T[f].append(t)
for f in sorted(fs, key=s):
    print(s(f), f)
def print_count(names_list):
    count = 0
    for name in names_list:
        
        for i in range(len(name)):
            if name[i] == 'i' and i == 1:
               
                count = count + 1

    print(count)

names = ["Cody", "Baldassar", "Delilah", "Vinnie", "Leila", "Zac", "Aiden", "Zaynab"]
print_count(names)
Related