Session 1 · Python in Google Colab
Day one at Cordillera Mobile¶
Today you join the analytics team of Cordillera Mobile, a Chilean mobile carrier with thousands of customers and two urgent questions. By the end of the class you will describe a customer in Python, apply the retention team's first rule to a list of customers, and open the real customer base for the first time.
01 Your first day¶
Welcome to the team. Two messages are already waiting for you:
- the commercial team asks: "What drives a customer's monthly bill?"
- the retention team asks: "Which customers are about to cancel, and who should we call?"
Over five sessions you will answer both, with Cordillera's own data. The tool is Python, written in a notebook: a page that mixes text cells (like this one) with code cells you can run. On this site, each code cell has a Run button and Python runs right in your browser. In Google Colab you press ▶ or Shift + Enter.
A code cell is a calculator that remembers. Run the next one.
# Anything after a # is a comment: Python ignores it.
monthly_bill = 31.9 # one customer's last bill, in USD
print("This customer pays", round(monthly_bill * 12, 2), "USD a year")
This customer pays 382.8 USD a year
Python now remembers monthly_bill, so any later cell can use it. That memory (the kernel) belongs to the session, not to the page: it only knows what you have run, in the order you ran it. Run cells out of order, run one twice, or delete one, and the memory no longer matches what you read on screen. Try it below.
Business takeaway: your notebook is a report that others will rerun. Before you share it (or hand in an advance), use Runtime → Restart session and run all in Colab (Run all on this site). If it runs top to bottom without errors, your teammates and your grader will see exactly what you saw.
02 Variables and types¶
Let's describe one customer, CM-10482, the way the billing system does: a plan, a bill, how long they have been with us, and whether they pay automatically. Each fact gets a variable: a name on the left of =, a value on the right.
A variable is a label, not a box: the value lives in memory and the name points at it. When you write tenure_months = tenure_months + 1, Python first computes the right side, then moves the label to the new value. Step through it.
customer_id = "CM-10482"
plan = "Plus"
monthly_bill = 31.9
tenure_months = 14
autopay = True
print(customer_id, plan, monthly_bill, tenure_months, autopay)
CM-10482 Plus 31.9 14 True
Every value has a type, and the type decides what you can do with it. You will meet four all the time:
| type | what it holds | Cordillera example |
|---|---|---|
str |
text, in quotes | "Plus", "CM-10482" |
int |
whole numbers | 14 months |
float |
numbers with decimals | 31.9 USD |
bool |
True or False |
autopay = True |
print(type(plan))
print(type(tenure_months))
print(type(monthly_bill))
print(type(autopay))
<class 'str'> <class 'int'> <class 'float'> <class 'bool'>
Data often arrives as text: a CSV export or a web form gives you "14", not 14. Text and numbers don't mix, so you convert with int(), float() or str().
To write sentences for a report, use an f-string: put an f before the quotes and any variable inside { }. Add :.2f to show two decimals, or :,.2f to also add a thousands separator.
tenure_text = "14" # as it came from a web form
try:
tenure_text + 1
except Exception as e:
print(type(e).__name__, "→", e)
print(int(tenure_text) + 1) # convert first, then add
print(float("31.9") * 2)
print("Months: " + str(tenure_months))
TypeError → can only concatenate str (not "int") to str 15 63.8 Months: 14
# f-strings: variables inside { }, formats after a colon
print(f"Customer {customer_id} is on the {plan} plan and has been with us {tenure_months} months.")
print(f"Last bill: {monthly_bill:.2f} USD. A year at this price: {monthly_bill * 12:,.2f} USD.")
Customer CM-10482 is on the Plus plan and has been with us 14 months. Last bill: 31.90 USD. A year at this price: 382.80 USD.
03 Lists: a customer's last six bills¶
One bill says little. The commercial team wants to see the trend: the last six monthly bills of CM-10482, April to September. A list keeps many values in order, inside square brackets.
Each item has a position, its index, and Python counts from 0. Negative indices count from the end, so bills[-1] is always the latest bill, however long the list is. A slice bills[start:stop] takes a range and never includes stop. Try some expressions.
months = ["Apr", "May", "Jun", "Jul", "Aug", "Sep"]
bills = [29.4, 30.8, 30.1, 35.6, 32.3, 31.9]
print("First month:", months[0], bills[0])
print("Latest month:", months[-1], bills[-1])
print("Last three bills:", bills[-3:])
plans = ["Prepaid", "Basic", "Plus", "Unlimited"] # lists can hold text too
print(len(plans), "plans; is 'Plus' one of them?", "Plus" in plans)
First month: Apr 29.4 Latest month: Sep 31.9 Last three bills: [35.6, 32.3, 31.9] 4 plans; is 'Plus' one of them? True
Lists come with tools that answer business questions in one line: how many items (len), the total (sum), the extremes (min, max).
average = sum(bills) / len(bills)
print(f"{len(bills)} bills, {sum(bills):.2f} USD in total")
print(f"Average bill: {average:.2f} USD (lowest {min(bills)}, highest {max(bills)})")
print("Highest month:", months[bills.index(max(bills))])
6 bills, 190.10 USD in total Average bill: 31.68 USD (lowest 29.4, highest 35.6) Highest month: Jul
The July spike (35.6 USD) is the kind of "bill shock" the retention team worries about. Lists can also grow: append adds an item at the end. October's bill just came in. We rebuild the list inside the cell on purpose: with a plain bills.append(33.0), every extra run of the cell would add October again (the "cell run twice" trap from the first explainer).
october = [29.4, 30.8, 30.1, 35.6, 32.3, 31.9] # a fresh copy of the six bills
october.append(33.0) # add October's bill
print(october, "→", len(october), "bills")
[29.4, 30.8, 30.1, 35.6, 32.3, 31.9, 33.0] → 7 bills
04 Dictionaries: one customer, one record¶
Five loose variables for one customer gets messy fast. A dictionary (dict) keeps all the facts about the customer together, each under a name called a key: customer["plan"] reads like a column name in a spreadsheet row.
You look a value up by its key, not by position. Asking for a key that isn't there raises a KeyError; .get(key, default) returns a default instead. Try both.
customer = {
"customer_id": "CM-10482",
"plan": "Plus",
"contract": "Month-to-month",
"monthly_bill": 31.9,
"tenure_months": 14,
"support_tickets": 2,
"autopay": True,
}
print(customer["plan"], customer["monthly_bill"])
print(f"{customer['customer_id']} has opened {customer['support_tickets']} support tickets.")
Plus 31.9 CM-10482 has opened 2 support tickets.
October has started, so CM-10482 is one month older as a customer. Assigning to an existing key replaces its value. The CRM has no region for this customer, so we ask for it safely with .get.
customer["tenure_months"] = 15 # update the record
print("Tenure now:", customer["tenure_months"], "months")
print("Region:", customer.get("region", "Unknown"))
try:
customer["region"]
except Exception as e:
print(type(e).__name__, "→", e)
print(len(customer), "fields:", list(customer.keys()))
Tenure now: 15 months Region: Unknown KeyError → 'region' 7 fields: ['customer_id', 'plan', 'contract', 'monthly_bill', 'tenure_months', 'support_tickets', 'autopay']
Now the key idea for the rest of the course: a list of dictionaries is a table. Each dict is a row, each key is a column. Here are five customers from the retention team's watch list.
customers = [
{"customer_id": "CM-10482", "plan": "Plus", "contract": "Month-to-month", "monthly_bill": 31.9, "support_tickets": 2},
{"customer_id": "CM-10517", "plan": "Unlimited", "contract": "24 months", "monthly_bill": 44.1, "support_tickets": 0},
{"customer_id": "CM-10533", "plan": "Basic", "contract": "Month-to-month", "monthly_bill": 22.75, "support_tickets": 4},
{"customer_id": "CM-10561", "plan": "Prepaid", "contract": "Month-to-month", "monthly_bill": 11.2, "support_tickets": 3},
{"customer_id": "CM-10598", "plan": "Plus", "contract": "12 months", "monthly_bill": 36.4, "support_tickets": 1},
]
print(len(customers), "customers")
print("Row 3:", customers[2])
print("Plan of row 3:", customers[2]["plan"]) # row 2 (the third), column "plan"
5 customers
Row 3: {'customer_id': 'CM-10533', 'plan': 'Basic', 'contract': 'Month-to-month', 'monthly_bill': 22.75, 'support_tickets': 4}
Plan of row 3: Basic
05 Conditionals: the retention team's first rule¶
The retention team has a rule of thumb, written on a whiteboard:
- at risk if the customer is on a month-to-month contract (free to leave any day) and has 3 or more support tickets;
- watch if only one of the two is true;
- ok otherwise.
In Python that is if / elif / else. Python checks the conditions from the top and runs only the first branch that is true; the rest are skipped. Move the controls and watch the path.
Conditions are built from comparisons (== equal, != different, >, >=, <, <=), which give True or False, combined with and (both must be true), or (at least one) and not.
contract = customer["contract"]
support_tickets = customer["support_tickets"]
print(contract == "Month-to-month")
print(support_tickets >= 3)
print(contract == "Month-to-month" and support_tickets >= 3)
True False False
if contract == "Month-to-month" and support_tickets >= 3:
risk = "at risk"
elif contract == "Month-to-month" or support_tickets >= 3:
risk = "watch"
else:
risk = "ok"
print(customer["customer_id"], "→", risk)
CM-10482 → watch
CM-10482 is month-to-month but has "only" 2 tickets, so they land on "watch": one more complaint and they would be at risk. Notice the indentation: the 4 spaces tell Python which lines belong to each branch. Now a different customer, on a 12-month contract with one ticket:
contract = "12 months"
support_tickets = 1
if contract == "Month-to-month" and support_tickets >= 3:
risk = "at risk"
elif contract == "Month-to-month" or support_tickets >= 3:
risk = "watch"
else:
risk = "ok"
print(risk)
ok
06 Loops: every customer, one rule¶
We just copied the whole rule to check a second customer; with five thousand customers, copying is not an option. Two numbers are due at Monday's meeting: the commercial team wants the total monthly revenue of the watch list, and the retention team wants how many customers are at risk.
A for loop runs the same lines once for each item of a list. The loop variable (c here) takes each customer in turn, and running totals, called accumulators, pile up. Step through it.
total = 0
at_risk = 0
for c in customers:
total = total + c["monthly_bill"]
if c["contract"] == "Month-to-month" and c["support_tickets"] >= 3:
at_risk = at_risk + 1
print(total, at_risk)
print(f"Monthly revenue of the list: {total:.2f} USD. Customers at risk: {at_risk} of {len(customers)}.")
146.35 2 Monthly revenue of the list: 146.35 USD. Customers at risk: 2 of 5.
# The same loop, one line per customer: the retention team's call sheet
for c in customers:
flag = "CALL" if c["contract"] == "Month-to-month" and c["support_tickets"] >= 3 else ""
print(f"{c['customer_id']} {c['plan']:<10} {c['monthly_bill']:>6.2f} USD {flag}")
CM-10482 Plus 31.90 USD CM-10517 Unlimited 44.10 USD CM-10533 Basic 22.75 USD CALL CM-10561 Prepaid 11.20 USD CALL CM-10598 Plus 36.40 USD
Python also has a compact way to build a list from a loop, the list comprehension. Read [c["monthly_bill"] for c in customers] as "the bill of every c in customers". Add if ... at the end to keep only some items. Toggle the explainer above to comprehension to watch it happen.
monthly_bills = [c["monthly_bill"] for c in customers]
print(monthly_bills)
print("Total:", sum(monthly_bills))
to_call = [c["customer_id"] for c in customers
if c["contract"] == "Month-to-month" and c["support_tickets"] >= 3]
print("Who should we call?", to_call)
[31.9, 44.1, 22.75, 11.2, 36.4] Total: 146.35 Who should we call? ['CM-10533', 'CM-10561']
Business takeaway: the loop and the comprehension give the same answer (146.35 USD, 2 customers to call). Use whichever you can read more easily; comprehensions shine for short "take this from every row" jobs.
07 Functions: write the rule once¶
We have now typed the retention rule three times. A function packages it under a name: you write it once with def, and call it for any customer. What goes in are the arguments, what comes out is the return value.
The commercial team needs a second function: a quick bill estimate from Cordillera's price list (plan base price plus 0.55 USD per GB of data, only 0.10 USD per GB on Unlimited, plus 8 USD for roaming). A parameter can have a default value, used when the caller leaves it out: watch the machine with roaming left out, then tick roaming=True.
def risk_level(customer):
"""Retention rule: 'at risk', 'watch' or 'ok'."""
month_to_month = customer["contract"] == "Month-to-month"
many_tickets = customer["support_tickets"] >= 3
if month_to_month and many_tickets:
return "at risk"
elif month_to_month or many_tickets:
return "watch"
else:
return "ok"
for c in customers:
print(c["customer_id"], "→", risk_level(c))
CM-10482 → watch CM-10517 → ok CM-10533 → at risk CM-10561 → at risk CM-10598 → ok
def estimate_bill(plan, data_gb, roaming=False):
base = {"Prepaid": 9, "Basic": 18, "Plus": 29, "Unlimited": 42}[plan]
per_gb = 0.10 if plan == "Unlimited" else 0.55
bill = base + per_gb * data_gb
if roaming:
bill = bill + 8
return round(bill, 2)
estimate = estimate_bill("Plus", 5.2)
print("Plus, 5.2 GB:", estimate)
print("Plus, 5.2 GB, roaming:", estimate_bill("Plus", 5.2, roaming=True))
print("Unlimited, 30 GB:", estimate_bill("Unlimited", 30))
Plus, 5.2 GB: 31.86 Plus, 5.2 GB, roaming: 39.86 Unlimited, 30 GB: 45.0
CM-10482 used 5.2 GB last month on Plus, so the price list says about 31.86 USD; the real bill was 31.9. The small gap comes from things the estimate ignores (call minutes, contract discounts). In session 3 you will let the data learn these prices on its own, with a regression model: that is the commercial team's question.
Functions combine with comprehensions, too: one line gives the risk level of the whole list.
levels = [risk_level(c) for c in customers]
print(levels)
print("At risk:", levels.count("at risk"), "| watch:", levels.count("watch"), "| ok:", levels.count("ok"))
['watch', 'ok', 'at risk', 'at risk', 'ok'] At risk: 2 | watch: 1 | ok: 2
08 NumPy: thousands of customers at once¶
Five customers fit in a loop. Cordillera has five thousand, and every month adds five thousand new bills. Python loops handle one item at a time, which gets slow at that scale.
NumPy stores numbers in an array and runs an operation on all of them at once, in fast compiled code: bills * 1.05 raises every price in one step, bills > 40 checks every bill in one step. Race the two approaches.
import numpy as np
bills = np.array([29.4, 30.8, 30.1, 35.6, 32.3, 31.9]) # CM-10482, April to September
print(bills * 1.05) # every bill after a 5% rise, no loop
print(bills.mean(), bills.max())
print(bills > 31) # which months were above 31 USD?
[30.87 32.34 31.605 37.38 33.915 33.495] 31.683333333333334 35.6 [False False False True True True]
Now the real scale. We simulate one month for 5,000 customers with Cordillera's price list (step 09 swaps the simulation for the real file). base_prices[plan_idx] looks up 5,000 base prices at once, and np.where picks the per-GB price for every customer: this is estimate_bill without a loop.
rng = np.random.default_rng(42)
n = 5000
plan_idx = rng.choice(4, size=n, p=[0.25, 0.30, 0.28, 0.17]) # 0 Prepaid, 1 Basic, 2 Plus, 3 Unlimited
data_gb = rng.gamma(4.0, 3.0, size=n).round(1)
base_prices = np.array([9, 18, 29, 42])
per_gb = np.where(plan_idx == 3, 0.10, 0.55)
month_bills = (base_prices[plan_idx] + per_gb * data_gb).round(2)
print(month_bills[:8])
print(f"{len(month_bills):,} bills, monthly revenue {month_bills.sum():,.2f} USD, average {month_bills.mean():.2f} USD")
[32.19 24.93 42.95 34.01 21.6 43.93 31.48 32.8 ] 5,000 bills, monthly revenue 142,033.33 USD, average 28.41 USD
Business questions become one-liners. What does a 5% price rise bring in? How many customers pay more than 40 USD? A comparison gives a boolean mask, an array of True/False; sum counts the Trues and mean gives their share.
extra = (month_bills * 1.05).sum() - month_bills.sum()
print(f"A 5% rise adds {extra:,.2f} USD a month")
high = month_bills > 40
print(f"{high.sum():,} customers pay more than 40 USD ({high.mean():.1%} of the base)")
print("Their average bill:", round(month_bills[high].mean(), 2))
A 5% rise adds 7,101.67 USD a month 968 customers pay more than 40 USD (19.4% of the base) Their average bill: 43.13
And the speed? Here is the same estimate done both ways. The exact times depend on your computer, but the gap is always large, and it grows with the data.
import time
plan_names = np.array(plans)[plan_idx] # 'Prepaid', 'Basic', ... for each customer
t0 = time.perf_counter()
loop_bills = [estimate_bill(p, d) for p, d in zip(plan_names, data_gb)]
t_loop = time.perf_counter() - t0
t0 = time.perf_counter()
array_bills = (base_prices[plan_idx] + per_gb * data_gb).round(2)
t_numpy = time.perf_counter() - t0
print(f"Python loop: {t_loop * 1000:.1f} ms | NumPy: {t_numpy * 1000:.2f} ms | about {t_loop / t_numpy:.0f}x faster")
print("Same answers?", np.allclose(loop_bills, array_bills))
Python loop: 11.7 ms | NumPy: 0.10 ms | about 115x faster Same answers? True
09 Meet the real data¶
Time for the real thing. Cordillera's customer base is a CSV file with one row per customer. To read it we use pandas, the library you will use every week from now on. A pandas DataFrame is a table: like our list of dictionaries, but each column is a NumPy array with a name.
First, our five customers as a DataFrame:
import pandas as pd
pd.DataFrame(customers)
| customer_id | plan | contract | monthly_bill | support_tickets | |
|---|---|---|---|---|---|
| 0 | CM-10482 | Plus | Month-to-month | 31.90 | 2 |
| 1 | CM-10517 | Unlimited | 24 months | 44.10 | 0 |
| 2 | CM-10533 | Basic | Month-to-month | 22.75 | 4 |
| 3 | CM-10561 | Prepaid | Month-to-month | 11.20 | 3 |
| 4 | CM-10598 | Plus | 12 months | 36.40 | 1 |
Now the full file. pd.read_csv reads it straight from its URL, so the notebook works for anyone who opens it, with nothing to upload. Before analysing anything, an analyst always runs the same first inspection: size, first rows, column types, missing values and duplicate rows.
RAW = "https://raw.githubusercontent.com/hizocar/usm-course-intensivo/main/docs/datasets/cordillera_mobile.csv"
df = pd.read_csv(RAW)
print(df.shape)
print(f"{df.shape[0]:,} rows (customers) and {df.shape[1]} columns")
(5075, 15) 5,075 rows (customers) and 15 columns
df.head()
| customer_id | signup_date | age | region | plan | contract | tenure_months | data_gb | call_minutes | intl_roaming | support_tickets | autopay | payment_method | monthly_bill | churned | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | CM-12254 | 2022-07-04 | 34 | Metropolitana | Basic | 12 months | 50 | NaN | 270 | 0 | 2 | 1 | NaN | 25.46 | 0 |
| 1 | CM-10173 | 2026-06-26 | 31 | Centro | Plus | 12 months | 3 | 19.0 | 310 | 0 | 0 | 0 | Debit card | 37.05 | 1 |
| 2 | CM-11339 | 2020-05-29 | 38 | Centro | Prepaid | Month-to-month | 76 | 3.6 | 165 | 0 | 0 | 1 | Credit card | 11.43 | 0 |
| 3 | CM-13440 | 2019-11-02 | 31 | Centro | Unlimited | 12 months | 82 | 17.8 | 248 | 0 | 2 | 0 | Debit card | 40.97 | 0 |
| 4 | CM-10214 | 2022-07-01 | 34 | Metropolitana | Basic | Month-to-month | 50 | 4.3 | 240 | 0 | 0 | 0 | Cash | 19.18 | 1 |
df.dtypes
customer_id object signup_date object age int64 region object plan object contract object tenure_months int64 data_gb float64 call_minutes int64 intl_roaming int64 support_tickets int64 autopay int64 payment_method object monthly_bill float64 churned int64 dtype: object
Each row is a customer, each column a fact about them. Two columns are the answers our two teams care about: monthly_bill (commercial team) and churned, 1 if the customer cancelled within three months (retention team).
In dtypes, int64 and float64 are numbers and object means text. Note that signup_date came in as text, not as a date: worth remembering. Next, the two counts every inspection needs, missing values per column and duplicate rows:
print("Duplicate rows:", df.duplicated().sum())
df.isna().sum()
Duplicate rows: 75
customer_id 0 signup_date 0 age 0 region 0 plan 0 contract 0 tenure_months 0 data_gb 201 call_minutes 0 intl_roaming 0 support_tickets 0 autopay 0 payment_method 154 monthly_bill 0 churned 0 dtype: int64
So: 201 customers without data_gb, 154 without payment_method, and 75 rows that appear twice. Look a little closer and there is more dirt:
print(df["region"].unique())
print(f"Bills go from {df['monthly_bill'].min()} to {df['monthly_bill'].max()} USD")
print(f"Mean bill {df['monthly_bill'].mean():.2f} USD vs median {df['monthly_bill'].median():.2f} USD")
['Metropolitana' 'Centro' 'Sur' 'Norte' 'Metropolitana ' 'norte' 'Sur ' 'sur' 'metropolitana' 'METROPOLITANA' 'NORTE'] Bills go from -36.08 to 4689.0 USD Mean bill 33.34 USD vs median 29.05 USD
"Norte", "NORTE" and "norte" are the same region typed three ways; a bill can't be negative, and nobody pays 4,689 USD a month on a mobile plan. Those few absurd bills are enough to pull the mean above the median.
Business takeaway: real data is never clean, and dirt changes the answers. Today we only look; we don't fix anything yet. Cleaning it, step by step and with each decision justified, is session 2.
10 Your project this week¶
Everything you did with Cordillera today is Advance 1 for your team's own case. Work on it in today's workshop; it is due Tuesday, November 3, at 23:59 (full checklist).
| Today at Cordillera Mobile | Your team does |
|---|---|
| You joined the analytics team | Register your team (2–3 members, team name, case) in the team mailbox. Own case (C)? Include the proposal. |
| We worked in one notebook | Create the project notebook in Colab, share it with the team (Editor) and with hizocar@gmail.com (Commenter). |
pd.read_csv(RAW) |
Load your case's file from its URL: employee_compensation.csv (A) or loan_default.csv (B); URLs on the datasets page. |
shape, head(), dtypes, isna().sum(), duplicated().sum() |
The same first inspection on your data. Write one sentence on what you see. |
Two questions: what drives monthly_bill, who will churn (churned) |
Your business question in 2–3 lines and who would use the answer: what drives Salary (A), who will default (B). |
Here is a starter cell. It runs on Cordillera's file; in your project notebook, change the URL to your case's file.
DATA_URL = RAW # in your notebook: your case's URL
data = pd.read_csv(DATA_URL)
print("Shape:", data.shape)
print("\nColumn types:\n", data.dtypes, sep="")
print("\nMissing values per column:\n", data.isna().sum(), sep="")
print("\nDuplicate rows:", data.duplicated().sum())
data.head()
Shape: (5075, 15) Column types: customer_id object signup_date object age int64 region object plan object contract object tenure_months int64 data_gb float64 call_minutes int64 intl_roaming int64 support_tickets int64 autopay int64 payment_method object monthly_bill float64 churned int64 dtype: object Missing values per column: customer_id 0 signup_date 0 age 0 region 0 plan 0 contract 0 tenure_months 0 data_gb 201 call_minutes 0 intl_roaming 0 support_tickets 0 autopay 0 payment_method 154 monthly_bill 0 churned 0 dtype: int64 Duplicate rows: 75
| customer_id | signup_date | age | region | plan | contract | tenure_months | data_gb | call_minutes | intl_roaming | support_tickets | autopay | payment_method | monthly_bill | churned | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | CM-12254 | 2022-07-04 | 34 | Metropolitana | Basic | 12 months | 50 | NaN | 270 | 0 | 2 | 1 | NaN | 25.46 | 0 |
| 1 | CM-10173 | 2026-06-26 | 31 | Centro | Plus | 12 months | 3 | 19.0 | 310 | 0 | 0 | 0 | Debit card | 37.05 | 1 |
| 2 | CM-11339 | 2020-05-29 | 38 | Centro | Prepaid | Month-to-month | 76 | 3.6 | 165 | 0 | 0 | 1 | Credit card | 11.43 | 0 |
| 3 | CM-13440 | 2019-11-02 | 31 | Centro | Unlimited | 12 months | 82 | 17.8 | 248 | 0 | 2 | 0 | Debit card | 40.97 | 0 |
| 4 | CM-10214 | 2022-07-01 | 34 | Metropolitana | Basic | Month-to-month | 50 | 4.3 | 240 | 0 | 0 | 0 | Cash | 19.18 | 1 |
Checklist before Tuesday: the notebook runs top to bottom after Restart session and run all; every output is visible; the business question is in your own words. New to Colab? See the Colab guide.
Next time at Cordillera Mobile¶
You saw the dirt: 75 duplicate rows, regions spelled eleven different ways, impossible ages, negative bills and one of 4,689 USD. In session 2 we clean the customer base decision by decision, then draw the first charts that start to answer both questions. Your team does the same with its own data for Advance 2.