Skip to content

Commit 9e275e6

Browse files
committed
This now should me mentally engaging.
1 parent d0ab147 commit 9e275e6

7 files changed

Lines changed: 426 additions & 172 deletions

File tree

docs/06-flow.md

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ for i in range(2):
9595
Print every second sample name:
9696

9797
```python
98-
samples = list(data["expression"].columns)
98+
samples = list(data["samples"].index)
9999

100100
for i in range(0, len(samples), 2):
101101
print(samples[i])
@@ -128,7 +128,7 @@ expr = data["expression"]
128128

129129
gene_means = []
130130

131-
for gene in range(len(expr)):
131+
for gene in range(expr.shape[0]):
132132
m = expr[gene].mean()
133133
gene_means.append(m)
134134

@@ -201,13 +201,15 @@ while w <= 5:
201201
This shows how `while` can stop early when a condition is met.
202202

203203
```python
204+
cluster1_samples = []
204205
i = 0
205-
samples = list(data["samples"].index)
206206

207-
while i < len(samples) and data["samples"].loc[samples[i], "cluster"] != 1:
207+
while i+1 < len(data["samples"]):
208+
if data["samples"]["cluster"].iloc[i] == 1:
209+
cluster1_samples.append(data["samples"].index[i])
208210
i = i + 1
209211

210-
samples[i]
212+
cluster1_samples
211213
```
212214

213215
---

docs/07-functions.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ def get_gene(data, gene):
8484
"""
8585
if gene not in data["genes"].index:
8686
raise ValueError(
87-
f"Unknown gene '{gene}'. Expected one of: {list(data["genes"].index)}"
87+
f"Unknown gene '{gene}'. Expected one of: {list(data['genes'].index)}"
8888
)
8989

9090
g_idx = data["genes"].index.get_loc(gene)
@@ -138,7 +138,7 @@ in the sample table equals `value`.
138138
samples_in(data, col_name, value ):
139139
"""
140140
Returns a list of samples wher <column_name> equals <value>
141-
""""
141+
"""
142142
samples = data["samples"]
143143

144144
if col_name not in samples.columns:

docs/09-performance.md

Lines changed: 44 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,16 @@ hspc_data = pd.read_csv(
3131
)
3232

3333
hspc_data
34+
35+
## and now force that into our data type
36+
def from_pd(expr):
37+
genes = pd.DataFrame(index=expr.index)
38+
samples = pd.DataFrame(index=expr.columns)
39+
return {"expression": np.array(expr), "genes": genes, "samples": samples}
40+
41+
hspc_data = from_pd(hspc_data )
42+
hspc_data
43+
3444
```
3545

3646
---
@@ -79,9 +89,9 @@ def euclid_distance_by_hand(v1, v2):
7989
return math.sqrt(s)
8090

8191

82-
def distance_matrix_df(df, func):
83-
X = np.asarray(df)
84-
names = df.index
92+
def distance_matrix_df(data, func):
93+
X = data['expression']
94+
names = data['genes'].index
8595
n = X.shape[0]
8696

8797
D = np.zeros((n, n))
@@ -94,10 +104,27 @@ def distance_matrix_df(df, func):
94104
return pd.DataFrame(D, index=names, columns=names)
95105
```
96106

97-
Time it:
107+
Time it - or better time a subset of the real data:
108+
109+
```python
110+
## actually first get us a subset of our data:
111+
def subset_genes(data, gene_idx):
112+
gene_idx = np.asarray(gene_idx)
113+
114+
X2 = data["expression"][ gene_idx,:]
115+
genes2 = data["genes"].iloc[gene_idx].copy()
116+
117+
# samples unchanged
118+
samples2 = data["samples"].copy()
119+
120+
return {"expression": X2, "genes": genes2, "samples": samples2}
121+
hspc_data_tiny = subset_genes( hspc_data , np.arange(200) )
122+
check_data_model( hspc_data_tiny )
123+
hspc_data_tiny.shape
124+
```
98125

99126
```python
100-
(_, t_slow) = timed(distance_matrix_df, hspc_data.iloc[range(200)], euclid_distance_by_hand )# first 200 genes of the whole data only
127+
(_, t_slow) = timed(distance_matrix_df, hspc_data_tiny, euclid_distance_by_hand )# first 200 genes of the whole data only
101128
print("slow:", t_slow)
102129
```
103130

@@ -119,7 +146,7 @@ def euclid_distance_vec(v1, v2):
119146
Time it:
120147

121148
```python
122-
(_, t_faster) = timed(distance_matrix_df, hspc_data.iloc[range(200)], euclid_distance_vec )
149+
(_, t_faster) = timed(distance_matrix_df, hspc_data_tiny , euclid_distance_vec )
123150
print("faster:", t_faster)
124151
```
125152

@@ -132,15 +159,13 @@ But there are also specialized function that operate on the full matrix:
132159
```python
133160
from scipy.spatial.distance import pdist, squareform
134161

135-
def vectorized_dist_mat(mat):
162+
def vectorized_dist(data):
136163
"""
137-
mat: pandas DataFrame or numpy array (rows = observations, cols = features)
138-
returns: full (n x n) Euclidean distance matrix as numpy array
164+
data: a dist with "expression" - a numpy array (rows = features, cols = observations)
165+
returns: full (nrow x nrow) Euclidean distance matrix as numpy array
139166
"""
140-
X = np.asarray(mat, dtype=float)
141-
142167
# upper triangle (condensed form)
143-
upper = pdist(X, metric="euclidean")
168+
upper = pdist(data['expression'], metric="euclidean")
144169

145170
# convert to full symmetric matrix
146171
D = squareform(upper)
@@ -164,70 +189,25 @@ If not 100% sure you know all about the possible libraries you could use I recom
164189

165190
---
166191

167-
# What about pandas apply?
168-
169-
In R, `apply()` can be a speed trick.
170-
In Python, `DataFrame.apply()` usually still runs a Python function once per row/column.
171-
172-
That means it often behaves like a loop (and can be slow). But eucledian distance is probably the worst example for apply as it works on one row/column only.
173-
So for this we switch to a simple mean calculation.
174-
175-
```python
176-
def mean_vec(vec1):
177-
return vec1.mean() # very fast here
178-
```
179-
180-
```python
181-
(_, t_vec) = timed( hspc_data.apply, mean_vec , axis=1)
182-
print("apply time:", t_vec)
183-
```
184-
185-
This is rather fast, but using the inbuild pandas mean function is even faster:
186-
187-
188-
```python
189-
(_, t_vec) = timed( hspc_data.mean, axis=1)
190-
print("pandas time:", t_vec)
191-
```
192-
193-
If we would have our data as a numpy array instead we could get this even faster using numpy's vectorization:
194-
195-
196-
```python
197-
arr = np.array(hspc_data)
198-
(c, t_fast) = timed(arr.mean, axis=1 )
199-
print("numpy:", t_fast)
200-
```
201-
202-
**Take Home** If numpy has a function for your problem use that!
203-
204-
---
205192

206193
# Interpreting the result
207194

208195
Usually:
209196

210197
- vectorized version is very fast
211-
- apply is much slower
212-
213-
Why? Because `apply` calls Python code repeatedly.
214-
215-
---
216-
217-
# When is apply OK?
218-
219-
`apply` can be reasonable when:
198+
- pure python as well as manual for loops are much slower
220199

221-
- there is no clean vectorized solution
222-
- the dataset is small
223-
- readability matters more than speed
200+
Why? Because even a for loop calls Python code repeatedly whereas the vectorized function (c or fortran) works on one big memory block.
224201

225-
But for large biological matrices (genes x samples), prefer vectorization.
226202

227203
---
228204

229205
# Exercise
230206

231-
Just keep the ``timed`` function and apply it later on whenever you like ;-)
207+
Take the function ``zscore_rows`` and convert it from using a numpy ndarray to using our own data structure.
208+
While doing that change tha action to modifying the data in place.
209+
210+
**Note:** Mutable objects (like lists, dictionaries, and arrays) can be changed inside a function, while immutable objects (like numbers and strings) cannot. Think of it like "Small objects like numbers or strings can be copied, but putatively large ones like matrices or dictionaries should not be copied".
211+
232212

233213
In the next section, we will apply these ideas to real expression data: selecting variable genes and scaling (z-scores).

docs/10-variable-genes.md

Lines changed: 33 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -52,25 +52,30 @@ So we often focus on the **top N most variable genes**.
5252
# Select the top variable genes
5353

5454
```python
55-
def get_top_variable_genes(mat, top_n=500):
56-
variances = mat.var(axis=1, ddof=1) # variance per row (gene)
57-
top_genes = variances.nlargest(top_n).index # gene names of the largest variances
58-
return mat.loc[top_genes]
55+
def get_top_variable_genes(data, top_n=500):
56+
mat = data['expression']
57+
var = mat.var(axis=1, ddof=1) # variance per row (gene)
58+
59+
# 2) get sorted gene indices (small → large variance)
60+
sorted_ids = np.argsort(var)
61+
62+
# return a subset (bottom top_n)
63+
return subset_genes(data, sorted_ids[-top_n:])
5964
```
6065

6166
Use it:
6267

6368
```python
6469
hspc_var = get_top_variable_genes(hspc_data, top_n=500)
65-
hspc_var.shape
70+
hspc_var['expression'].shape
6671
```
6772

6873
---
6974

7075
# Check the value range
7176

7277
```python
73-
hspc_var.values.min(), hspc_var.values.max()
78+
hspc_var['expression'].min(), hspc_var['expression'].max()
7479
```
7580

7681
---
@@ -100,16 +105,17 @@ This makes genes comparable even if they have very different expression ranges.
100105

101106
```python
102107
def zscore_rows(mat):
103-
m = mat.mean(axis=1)
104-
s = mat.std(axis=1, ddof=1)
105-
return mat.sub(m, axis=0).div(s, axis=0)
108+
m = np.mean(mat, axis=1, keepdims=True)
109+
s = np.std(mat, axis=1, ddof=1, keepdims=True)
110+
return (mat - m) / s
106111
```
107112

108113
Run it:
109114

110115
```python
111-
hspc_zs = zscore_rows(hspc_var)
112-
hspc_zs.shape
116+
hspc_zs = hspc_data_tiny.copy()
117+
hspc_zs['expression'] = zscore_rows(hspc_data_tiny['expression'])
118+
hspc_zs['expression'].shape
113119
```
114120

115121
---
@@ -133,18 +139,30 @@ print(hspc_zs.std(axis=1, ddof=1).head())
133139

134140
# Visual check: boxplots before and after scaling
135141

142+
There is a boxplot function in pandas, but our dict strores the data as numpy array.
143+
The easiest here is to define one more function that actually converts out dict to a pandas DatFrame - the reverse of our from_df function earlier:
144+
145+
```python
146+
def expression_df(data):
147+
return pd.DataFrame(
148+
data["expression"],
149+
columns=data["samples"].index,
150+
index=data["genes"].index
151+
)
152+
```
153+
136154
Before:
137155

138156
```python
139-
hspc_var.boxplot(rot=90)
157+
expression_df(hspc_var).T.boxplot(rot=90)
140158
plt.title("Before scaling (raw values)")
141159
plt.show()
142160
```
143161

144162
After:
145163

146164
```python
147-
hspc_zs.boxplot(rot=90)
165+
expression_df(hspc_zs).T.boxplot(rot=90)
148166
plt.title("After scaling (z-scores)")
149167
plt.show()
150168
```
@@ -153,9 +171,8 @@ plt.show()
153171

154172
# Exercise
155173

156-
1. Select the top 200 variable genes into `hspc_var200`.
157-
2. Z-score them into `hspc_zs200`.
158-
3. Verify that row means are ~0 and row standard deviations are ~1.
174+
We used ``expression_df(hspc_var).T`` there, but we likely also need a transform for our own data.
175+
Implement a ``def transform()`` that returns a transformed data structure.
159176

160177
---
161178

0 commit comments

Comments
 (0)